blob: ae7e7e0a69eeb566bce4452b858817595f554ad3 [file] [log] [blame]
// Copyright 2018 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.
// This file contain low level utils, i.e. utils that do not depend on
// libraries in this package.
import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:math';
import 'package:ansi_up/ansi_up.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:intl/intl.dart';
import 'package:vm_service/vm_service.dart';
bool isPrivate(String member) => member.startsWith('_');
/// Public properties first, then sort alphabetically
int sortFieldsByName(String a, String b) {
final isAPrivate = isPrivate(a);
final isBPrivate = isPrivate(b);
if (isAPrivate && !isBPrivate) {
return 1;
}
if (!isAPrivate && isBPrivate) {
return -1;
}
return a.compareTo(b);
}
bool collectionEquals(e1, e2, {bool ordered = true}) {
if (ordered) {
return const DeepCollectionEquality().equals(e1, e2);
}
return const DeepCollectionEquality.unordered().equals(e1, e2);
}
// 2^52 is the max int for dart2js.
final int maxJsInt = pow(2, 52) as int;
String escape(String? text) => text == null ? '' : htmlEscape.convert(text);
final NumberFormat nf = NumberFormat.decimalPattern();
String percent2(double d) => '${(d * 100).toStringAsFixed(2)}%';
String? prettyPrintBytes(
num? bytes, {
int kbFractionDigits = 0,
int mbFractionDigits = 1,
int gbFractionDigits = 1,
bool includeUnit = false,
num roundingPoint = 1.0,
int maxBytes = 52,
}) {
if (bytes == null) {
return null;
}
// TODO(peterdjlee): Generalize to handle different kbFractionDigits.
// Ensure a small number of bytes does not print as 0 KB.
// If bytes >= maxBytes and kbFractionDigits == 1, it will start rounding to 0.1 KB.
if (bytes.abs() < maxBytes && kbFractionDigits == 1) {
var output = bytes.toString();
if (includeUnit) {
output += ' B';
}
return output;
}
final sizeInKB = bytes.abs() / 1024.0;
final sizeInMB = sizeInKB / 1024.0;
final sizeInGB = sizeInMB / 1024.0;
if (sizeInGB >= roundingPoint) {
return '${printGB(bytes, fractionDigits: gbFractionDigits, includeUnit: includeUnit)}';
} else if (sizeInMB >= roundingPoint) {
return '${printMB(bytes, fractionDigits: mbFractionDigits, includeUnit: includeUnit)}';
} else {
return '${printKB(bytes, fractionDigits: kbFractionDigits, includeUnit: includeUnit)}';
}
}
String printKB(num bytes, {int fractionDigits = 0, bool includeUnit = false}) {
final NumberFormat _kbPattern = NumberFormat.decimalPattern()
..maximumFractionDigits = fractionDigits;
// We add ((1024/2)-1) to the value before formatting so that a non-zero byte
// value doesn't round down to 0. If showing decimal points, let it round normally.
// TODO(peterdjlee): Round up to the respective digit when fractionDigits > 0.
final processedBytes = fractionDigits == 0 ? bytes + 511 : bytes;
var output = _kbPattern.format(processedBytes / 1024);
if (includeUnit) {
output += ' KB';
}
return output;
}
String printMB(num bytes, {int fractionDigits = 1, bool includeUnit = false}) {
var output = (bytes / (1024 * 1024.0)).toStringAsFixed(fractionDigits);
if (includeUnit) {
output += ' MB';
}
return output;
}
String printGB(num bytes, {int fractionDigits = 1, bool includeUnit = false}) {
var output =
(bytes / (1024 * 1024.0 * 1024.0)).toStringAsFixed(fractionDigits);
if (includeUnit) {
output += ' GB';
}
return output;
}
/// Converts a [Duration] into a readable text representation in milliseconds.
///
/// [includeUnit] - whether to include 'ms' at the end of the returned value
/// [fractionDigits] - how many fraction digits should appear after the decimal
/// [allowRoundingToZero] - when true, this method may return zero for a very
/// small number (e.g. '0.0 ms'). When false, this method will return a minimum
/// value with the less than operator for very small values (e.g. '< 0.1 ms').
/// The value returned will always respect the specified [fractionDigits].
String msText(
Duration dur, {
bool includeUnit = true,
int fractionDigits = 1,
bool allowRoundingToZero = true,
}) {
var durationStr = (dur.inMicroseconds / 1000).toStringAsFixed(fractionDigits);
if (dur != Duration.zero && !allowRoundingToZero) {
final zeroRegexp = RegExp(r'[0]+[.][0]+');
if (zeroRegexp.hasMatch(durationStr)) {
final buf = StringBuffer('< 0.');
for (int i = 1; i < fractionDigits; i++) {
buf.write('0');
}
buf.write('1');
durationStr = buf.toString();
}
}
return '$durationStr${includeUnit ? ' ms' : ''}';
}
/// Render the given [Duration] to text using either seconds or milliseconds as
/// the units, depending on the value of the duration.
String renderDuration(Duration duration) {
if (duration.inMilliseconds < 1000) {
return '${nf.format(duration.inMilliseconds)}ms';
} else {
return '${(duration.inMilliseconds / 1000).toStringAsFixed(1)}s';
}
}
T? nullSafeMin<T extends num>(T? a, T? b) {
if (a == null || b == null) {
return a ?? b;
}
return min<T>(a, b);
}
T? nullSafeMax<T extends num>(T? a, T? b) {
if (a == null || b == null) {
return a ?? b;
}
return max<T>(a, b);
}
double logBase({required int x, required int base}) {
return log(x) / log(base);
}
int log2(num x) => (logBase(x: x.floor(), base: 2)).floor();
int roundToNearestPow10(int x) =>
pow(10, logBase(x: x, base: 10).ceil()).floor();
String isolateName(IsolateRef ref) {
// analysis_server.dart.snapshot$main
String name = ref.name!;
name = name.replaceFirst(r'.snapshot', '');
if (name.contains(r'.dart$')) {
name = name + '()';
}
return name;
}
String? funcRefName(FuncRef ref) {
if (ref.owner is LibraryRef) {
//(ref.owner as LibraryRef).uri;
return ref.name;
} else if (ref.owner is ClassRef) {
return '${ref.owner.name}.${ref.name}';
} else if (ref.owner is FuncRef) {
return '${funcRefName(ref.owner as FuncRef)}.${ref.name}';
} else {
return ref.name;
}
}
void executeWithDelay(
Duration delay,
void callback(), {
bool executeNow = false,
}) {
if (executeNow || delay.inMilliseconds <= 0) {
callback();
} else {
Timer(delay, () {
callback();
});
}
}
Future<void> delayForBatchProcessing({int micros = 0}) async {
// Even with a delay of 0 microseconds, awaiting this delay is enough to free
// the UI thread to update the UI.
await Future.delayed(Duration(microseconds: micros));
}
/// Creates a [Future] that completes either when `operation` completes or the
/// duration specified by `timeoutMillis` has passed.
///
/// Completes with null on timeout.
Future<T?> timeout<T>(Future<T> operation, int timeoutMillis) =>
Future.any<T?>([
operation,
Future<T?>.delayed(
Duration(milliseconds: timeoutMillis),
() => Future<T?>.value(),
)
]);
String longestFittingSubstring(
String originalText,
num maxWidth,
List<num> asciiMeasurements,
num slowMeasureFallback(int value),
) {
if (originalText.isEmpty) return originalText;
final runes = originalText.runes.toList();
num currentWidth = 0;
int i = 0;
while (i < runes.length) {
final rune = runes[i];
final charWidth =
rune < 128 ? asciiMeasurements[rune] : slowMeasureFallback(rune);
if (currentWidth + charWidth > maxWidth) {
break;
}
// [currentWidth] is approximate due to ignoring kerning.
currentWidth += charWidth;
i++;
}
return originalText.substring(0, i);
}
/// Whether a given code unit is a letter (A-Z or a-z).
bool isLetter(int codeUnit) =>
(codeUnit >= 65 && codeUnit <= 90) || (codeUnit >= 97 && codeUnit <= 122);
/// Pluralizes a word, following english rules (1, many).
///
/// Pass a custom named `plural` for irregular plurals:
/// `pluralize('index', count, plural: 'indices')`
/// So it returns `indices` and not `indexs`.
String pluralize(String word, int count, {String? plural}) =>
count == 1 ? word : (plural ?? '${word}s');
/// Returns a simplified version of a StackFrame name.
///
/// Given an input such as
/// `_WidgetsFlutterBinding&BindingBase&GestureBinding.handleBeginFrame`, this
/// method will strip off all the leading class names and return
/// `GestureBinding.handleBeginFrame`.
///
/// See (https://github.com/dart-lang/sdk/issues/36999).
String getSimpleStackFrameName(String name) {
final newName = name.replaceAll('<anonymous closure>', '<closure>');
// If the class name contains a space, then it is not a valid Dart name. We
// throw out simplified names with spaces to prevent simplifying C++ class
// signatures, where the '&' char signifies a reference variable - not
// appended class names.
if (newName.contains(' ')) {
return newName;
}
return newName.split('&').last;
}
/// Return a Stream that fires events whenever any of the three given parameter
/// streams fire.
Stream combineStreams(Stream a, Stream b, Stream c) {
late StreamController controller;
StreamSubscription? asub;
StreamSubscription? bsub;
StreamSubscription? csub;
controller = StreamController(
onListen: () {
asub = a.listen(controller.add);
bsub = b.listen(controller.add);
csub = c.listen(controller.add);
},
onCancel: () {
asub?.cancel();
bsub?.cancel();
csub?.cancel();
},
);
return controller.stream;
}
/// Parses a 3 or 6 digit CSS Hex Color into a dart:ui Color.
Color parseCssHexColor(String input) {
// Remove any leading # (and the escaped version to be lenient)
input = input.replaceAll('#', '').replaceAll('%23', '');
// Handle 3/4-digit hex codes (eg. #123 == #112233)
if (input.length == 3 || input.length == 4) {
input = input.split('').map((c) => '$c$c').join();
}
// Pad alpha with FF.
if (input.length == 6) {
input = '${input}ff';
}
// In CSS, alpha is in the lowest bits, but for Flutter's value, it's in the
// highest bits, so move the alpha from the end to the start before parsing.
if (input.length == 8) {
input = '${input.substring(6)}${input.substring(0, 6)}';
}
final value = int.parse(input, radix: 16);
return Color(value);
}
/// Converts a dart:ui Color into #RRGGBBAA format for use in CSS.
String toCssHexColor(Color color) {
// In CSS Hex, Alpha comes last, but in Flutter's `value` field, alpha is
// in the high bytes, so just using `value.toRadixString(16)` will put alpha
// in the wrong position.
String hex(int val) => val.toRadixString(16).padLeft(2, '0');
return '#${hex(color.red)}${hex(color.green)}${hex(color.blue)}${hex(color.alpha)}';
}
class Property<T> {
Property(this._value);
final StreamController<T> _changeController = StreamController<T>.broadcast();
T _value;
T get value => _value;
set value(T newValue) {
if (newValue != _value) {
_value = newValue;
_changeController.add(newValue);
}
}
Stream<T> get onValueChange => _changeController.stream;
}
/// Batch up calls to the given closure. Repeated calls to [invoke] will
/// overwrite the closure to be called. We'll delay at least [minDelay] before
/// calling the closure, but will not delay more than [maxDelay].
class DelayedTimer {
DelayedTimer(this.minDelay, this.maxDelay);
final Duration minDelay;
final Duration maxDelay;
VoidCallback? _closure;
Timer? _minTimer;
Timer? _maxTimer;
void invoke(VoidCallback closure) {
_closure = closure;
if (_minTimer == null) {
_minTimer = Timer(minDelay, _fire);
_maxTimer = Timer(maxDelay, _fire);
} else {
_minTimer!.cancel();
_minTimer = Timer(minDelay, _fire);
}
}
void _fire() {
_minTimer?.cancel();
_minTimer = null;
_maxTimer?.cancel();
_maxTimer = null;
_closure!();
_closure = null;
}
}
/// These utilities are ported from the Flutter IntelliJ plugin.
///
/// With Dart's terser JSON support, these methods don't provide much value so
/// we should consider removing them.
class JsonUtils {
JsonUtils._();
static String? getStringMember(Map<String, Object?> json, String memberName) {
// TODO(jacobr): should we handle non-string values with a reasonable
// toString differently?
return json[memberName] as String?;
}
static int getIntMember(Map<String, Object?> json, String memberName) {
return json[memberName] as int? ?? -1;
}
static List<String> getValues(Map<String, Object> json, String member) {
final List<dynamic>? values = json[member] as List?;
if (values == null || values.isEmpty) {
return const [];
}
return values.cast();
}
static bool hasJsonData(String? data) {
return data != null && data.isNotEmpty && data != 'null';
}
}
/// Add pretty print for a JSON payload.
extension JsonMap on Map<String, Object?> {
String prettyPrint() => const JsonEncoder.withIndent(' ').convert(this);
}
typedef RateLimiterCallback = Future<void> Function();
/// Rate limiter that ensures a [callback] is run no more than the
/// specified rate and that at most one async [callback] is running at a time.
class RateLimiter {
RateLimiter(double requestsPerSecond, this.callback)
: delayBetweenRequests = 1000 ~/ requestsPerSecond;
final RateLimiterCallback callback;
Completer<void>? _pendingRequest;
/// A request has been scheduled to run but is not yet pending.
bool requestScheduledButNotStarted = false;
int? _lastRequestTime;
final int delayBetweenRequests;
Timer? _activeTimer;
/// Schedules the callback to be run the next time the rate limiter allows it.
///
/// If multiple calls to scheduleRequest are made before a request is allowed,
/// only a single request will be made.
void scheduleRequest() {
if (requestScheduledButNotStarted) {
// No need to schedule a request if one has already been scheduled but
// hasn't yet actually started executing.
return;
}
if (_pendingRequest != null && !_pendingRequest!.isCompleted) {
// Wait for the pending request to be done before scheduling the new
// request. The existing request has already started so may return state
// that is now out of date.
requestScheduledButNotStarted = true;
_pendingRequest!.future.whenComplete(() {
_pendingRequest = null;
requestScheduledButNotStarted = false;
scheduleRequest();
});
return;
}
final currentTime = DateTime.now().millisecondsSinceEpoch;
if (_lastRequestTime == null ||
_lastRequestTime! + delayBetweenRequests <= currentTime) {
// Safe to perform the request immediately.
_performRequest();
return;
}
// Track that we have scheduled a request and then schedule the request
// to occur once the rate limiter is available.
requestScheduledButNotStarted = true;
_activeTimer = Timer(
Duration(
milliseconds: currentTime - _lastRequestTime! + delayBetweenRequests,
), () {
_activeTimer = null;
requestScheduledButNotStarted = false;
_performRequest();
});
}
void _performRequest() async {
try {
_lastRequestTime = DateTime.now().millisecondsSinceEpoch;
_pendingRequest = Completer();
await callback();
} finally {
_pendingRequest!.complete(null);
}
}
void dispose() {
_activeTimer?.cancel();
}
}
/// Time unit for displaying time ranges.
///
/// If the need arises, this enum can be expanded to include any of the
/// remaining time units supported by [Duration] - (seconds, minutes, etc.). If
/// you add a unit of time to this enum, modify the toString() method in
/// [TimeRange] to handle the new case.
enum TimeUnit {
microseconds,
milliseconds,
}
class TimeRange {
TimeRange({this.singleAssignment = true});
final bool singleAssignment;
Duration? get start => _start;
Duration? _start;
set start(Duration? value) {
if (singleAssignment) {
assert(_start == null);
}
if (_end != null) {
assert(
value! <= _end!,
'$value is not less than or equal to end time $_end',
);
}
_start = value;
}
Duration? get end => _end;
Duration? _end;
set end(Duration? value) {
if (singleAssignment) {
assert(_end == null);
}
if (_start != null) {
assert(
value! >= _start!,
'$value is not greater than or equal to start time $_start',
);
}
_end = value;
}
Duration get duration => end! - start!;
bool contains(Duration target) => start! <= target && end! >= target;
bool containsRange(TimeRange t) => start! <= t.start! && end! >= t.end!;
bool overlaps(TimeRange t) => t.end! > start! && t.start! < end!;
bool get isWellFormed => _start != null && _end != null;
@override
String toString({TimeUnit? unit}) {
unit ??= TimeUnit.microseconds;
switch (unit) {
case TimeUnit.microseconds:
return '[${_start?.inMicroseconds} μs - ${end?.inMicroseconds} μs]';
case TimeUnit.milliseconds:
default:
return '[${_start?.inMilliseconds} ms - ${end?.inMilliseconds} ms]';
}
}
@override
bool operator ==(other) {
if (other is! TimeRange) return false;
return start == other.start && end == other.end;
}
@override
int get hashCode => Object.hash(start, end);
}
String formatDateTime(DateTime time) {
return DateFormat('H:mm:ss.S').format(time);
}
bool isDebugBuild() {
bool debugBuild = false;
assert(
(() {
debugBuild = true;
return true;
})(),
);
return debugBuild;
}
/// Divides [numerator] by [denominator], not returning infinite, NaN, or null
/// quotients.
///
/// Returns [ifNotFinite] as a return value when the result of dividing
/// [numerator] by [denominator] would be a non-finite value: either
/// NaN, null, or infinite.
///
/// [ifNotFinite] defaults to 0.0.
double safeDivide(
num? numerator,
num? denominator, {
double ifNotFinite = 0.0,
}) {
if (numerator != null && denominator != null) {
final quotient = numerator / denominator;
if (quotient.isFinite) {
return quotient;
}
}
return ifNotFinite;
}
/// A change reporter that can be listened to.
///
/// Unlike [ChangeNotifier], [Reporter] stores listeners in a set. This allows
/// O(1) addition/removal of listeners and O(N) listener dispatch.
///
/// For small N (~ <20), [ChangeNotifier] implementations can be faster because
/// array access is more efficient than set access. Use [Reporter] instead in
/// cases where N is larger.
///
/// When disposing, any object with a registered listener should [unregister]
/// itself.
///
/// Only the object that created this reporter should call [notify].
class Reporter implements Listenable {
final Set<VoidCallback> _listeners = {};
/// Adds [callback] to this reporter.
///
/// If [callback] is already registered to this reporter, nothing will happen.
@override
void addListener(VoidCallback callback) {
_listeners.add(callback);
}
/// Removes the listener [callback].
@override
void removeListener(VoidCallback callback) {
_listeners.remove(callback);
}
/// Whether or not this object has any listeners registered.
bool get hasListeners => _listeners.isNotEmpty;
/// Notifies all listeners of a change.
///
/// This does not do any change propagation, so if
/// a notification callback leads to a change in the listeners,
/// only the original listeners will be called.
void notify() {
for (var callback in _listeners.toList()) {
callback();
}
}
@override
String toString() => '${describeIdentity(this)}';
}
/// A [Reporter] that notifies when its [value] changes.
///
/// Similar to [ValueNotifier], but with the same performance
/// benefits as [Reporter].
///
/// For small N (~ <20), [ValueNotifier] implementations can be faster because
/// array access is more efficient than set access. Use [ValueReporter] instead
/// in cases where N is larger.
class ValueReporter<T> extends Reporter implements ValueListenable<T> {
ValueReporter(this._value);
@override
T get value => _value;
set value(T value) {
if (_value == value) return;
_value = value;
notify();
}
T _value;
@override
String toString() => '${describeIdentity(this)}($value)';
}
String toStringAsFixed(double num, [int fractionDigit = 1]) {
return num.toStringAsFixed(fractionDigit);
}
/// A value notifier that calls each listener immediately when registered.
class ImmediateValueNotifier<T> extends ValueNotifier<T> {
ImmediateValueNotifier(T value) : super(value);
/// Adds a listener and calls the listener upon registration.
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
listener();
}
}
extension SafeAccessList<T> on List<T> {
T? safeGet(int index) => index < 0 || index >= length ? null : this[index];
}
extension SafeAccess<T> on Iterable<T> {
T? get safeFirst => isNotEmpty ? first : null;
T? get safeLast => isNotEmpty ? last : null;
}
/// Range class for all nums (double and int).
///
/// Only operations that work on both double and int should be added to this
/// class.
class Range {
const Range(this.begin, this.end) : assert(begin <= end);
final num begin;
final num end;
num get size => end - begin;
bool contains(num target) => target >= begin && target <= end;
@override
String toString() => 'Range($begin, $end)';
@override
bool operator ==(other) {
if (other is! Range) return false;
return begin == other.begin && end == other.end;
}
@override
int get hashCode => Object.hash(begin, end);
}
enum SortDirection {
ascending,
descending,
}
extension SortDirectionExtension on SortDirection {
SortDirection reverse() {
return this == SortDirection.ascending
? SortDirection.descending
: SortDirection.ascending;
}
}
/// A small double value, used to ensure that comparisons between double are
/// valid.
const defaultEpsilon = 1 / 1000;
bool equalsWithinEpsilon(double a, double b) {
return (a - b).abs() < defaultEpsilon;
}
/// A dev time class to help trace DevTools application events.
class DebugTimingLogger {
DebugTimingLogger(this.name, {this.mute = false});
final String name;
final bool mute;
Stopwatch? _timer;
void log(String message) {
if (mute) return;
if (_timer != null) {
_timer!.stop();
print('[$name}] ${_timer!.elapsedMilliseconds}ms');
_timer!.reset();
}
_timer ??= Stopwatch();
_timer!.start();
print('[$name] $message');
}
}
/// Compute a simple moving average.
/// [averagePeriod] default period is 50 units collected.
/// [ratio] default percentage is 50% range is 0..1
class MovingAverage {
MovingAverage({
this.averagePeriod = 50,
this.ratio = .5,
List<int>? newDataSet,
}) : assert(ratio >= 0 && ratio <= 1, 'Value ratio $ratio is not 0 to 1.') {
if (newDataSet != null) {
var initialDataSet = newDataSet;
final count = newDataSet.length;
if (count > averagePeriod) {
initialDataSet = newDataSet.sublist(count - averagePeriod);
}
dataSet.addAll(initialDataSet);
for (final value in dataSet) {
averageSum += value;
}
}
}
final dataSet = Queue<int>();
/// Total collected items in the X axis (time) used to compute moving average.
/// Default 100 periods for memory profiler 1-2 periods / seconds.
final int averagePeriod;
/// Ratio of first item in dataSet when comparing to last - mean
/// e.g., 2 is 50% (dataSet.first ~/ ratioSpike).
final double ratio;
/// Sum of total heap used and external heap for unitPeriod.
int averageSum = 0;
/// Reset moving average data.
void clear() {
dataSet.clear();
averageSum = 0;
}
// Update the sum to get a new mean.
void add(int value) {
averageSum += value;
dataSet.add(value);
// Update dataSet of values to not exceede the period of the moving average
// to compute the normal mean.
if (dataSet.length > averagePeriod) {
averageSum -= dataSet.removeFirst();
}
}
double get mean {
final periodRange = min(averagePeriod, dataSet.length);
return periodRange > 0 ? averageSum / periodRange : 0;
}
/// If the last - mean > ratioSpike% of first value in period we're spiking.
bool hasSpike() {
final first = dataSet.safeFirst ?? 0;
final last = dataSet.safeLast ?? 0;
return last - mean > (first * ratio);
}
/// If the mean @ ratioSpike% > last value in period we're dipping.
bool isDipping() {
final last = dataSet.safeLast ?? 0;
return (mean * ratio) > last;
}
}
List<TextSpan> processAnsiTerminalCodes(String? input, TextStyle defaultStyle) {
if (input == null) {
return [];
}
return decodeAnsiColorEscapeCodes(input, AnsiUp())
.map(
(entry) => TextSpan(
text: entry.text,
style: entry.style.isEmpty
? defaultStyle
: TextStyle(
color: entry.fgColor != null
? colorFromAnsi(entry.fgColor!)
: null,
backgroundColor: entry.bgColor != null
? colorFromAnsi(entry.bgColor!)
: null,
fontWeight: entry.bold ? FontWeight.bold : FontWeight.normal,
),
),
)
.toList();
}
Color colorFromAnsi(List<int> ansiInput) {
assert(ansiInput.length == 3, 'Ansi color list should contain 3 elements');
return Color.fromRGBO(ansiInput[0], ansiInput[1], ansiInput[2], 1);
}
/// An extension on [LogicalKeySet] to provide user-facing names for key
/// bindings.
extension LogicalKeySetExtension on LogicalKeySet {
static final Set<LogicalKeyboardKey> _modifiers = {
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.meta,
LogicalKeyboardKey.shift,
};
static final Map<LogicalKeyboardKey, String> _modifierNames = {
LogicalKeyboardKey.alt: 'Alt',
LogicalKeyboardKey.control: 'Control',
LogicalKeyboardKey.meta: 'Meta',
LogicalKeyboardKey.shift: 'Shift',
};
/// Return a user-facing name for the [LogicalKeySet].
String describeKeys({bool isMacOS = false}) {
// Put the modifiers first. If it has a synonym, then it's something like
// shiftLeft, altRight, etc.
final List<LogicalKeyboardKey> sortedKeys = keys.toList()
..sort((a, b) {
final aIsModifier = a.synonyms.isNotEmpty || _modifiers.contains(a);
final bIsModifier = b.synonyms.isNotEmpty || _modifiers.contains(b);
if (aIsModifier && !bIsModifier) {
return -1;
} else if (bIsModifier && !aIsModifier) {
return 1;
}
return a.keyLabel.compareTo(b.keyLabel);
});
return sortedKeys.map((key) {
if (_modifiers.contains(key)) {
if (isMacOS && key == LogicalKeyboardKey.meta) {
// TODO(https://github.com/flutter/devtools/issues/3352) Switch back
// to using ⌘ once supported on web.
return kIsWeb ? 'Command-' : '⌘';
}
return '${_modifierNames[key]}-';
} else {
return key.keyLabel.toUpperCase();
}
}).join();
}
}
// Method to convert degrees to radians
double degToRad(num deg) => deg * (pi / 180.0);
typedef DevToolsJsonFileHandler = void Function(DevToolsJsonFile file);
class DevToolsJsonFile extends DevToolsFile<Object> {
const DevToolsJsonFile({
required String name,
required DateTime lastModifiedTime,
required Object data,
}) : super(
path: name,
lastModifiedTime: lastModifiedTime,
data: data,
);
}
class DevToolsFile<T> {
const DevToolsFile({
required this.path,
required this.lastModifiedTime,
required this.data,
});
final String path;
final DateTime lastModifiedTime;
final T data;
}
final _lowercaseLookup = <String, String>{};
// TODO(kenz): consider moving other String helpers into this extension.
// TODO(kenz): replace other uses of toLowerCase() for string matching with
// this extension method.
extension StringExtension on String {
bool caseInsensitiveContains(Pattern? pattern) {
if (pattern is RegExp) {
assert(pattern.isCaseSensitive == false);
return contains(pattern);
} else if (pattern is String) {
final lowerCase = _lowercaseLookup.putIfAbsent(this, () => toLowerCase());
final strLowerCase =
_lowercaseLookup.putIfAbsent(pattern, () => pattern.toLowerCase());
return lowerCase.contains(strLowerCase);
}
throw Exception(
'Unhandled pattern type ${pattern.runtimeType} from '
'`caseInsensitiveContains`',
);
}
/// Whether [query] is a case insensitive "fuzzy match" for this String.
///
/// For example, the query "hwf" would be a fuzzy match for the String
/// "hello_world_file".
bool caseInsensitiveFuzzyMatch(String query) {
query = query.toLowerCase();
final lowercase = toLowerCase();
final it = query.characters.iterator;
var strIndex = 0;
while (it.moveNext()) {
final char = it.current;
var foundChar = false;
for (int i = strIndex; i < lowercase.length; i++) {
if (lowercase[i] == char) {
strIndex = i + 1;
foundChar = true;
break;
}
}
if (!foundChar) {
return false;
}
}
return true;
}
/// Whether [other] is a case insensitive match for this String
bool caseInsensitiveEquals(String? other) {
return toLowerCase() == other?.toLowerCase();
}
/// Find all case insensitive matches of query in this String
/// See [allMatches] for more info
Iterable<Match> caseInsensitiveAllMatches(String? query) {
if (query == null) return const [];
return toLowerCase().allMatches(query.toLowerCase());
}
}
extension ListExtension<T> on List<T> {
List<T> joinWith(T separator) {
return [
for (int i = 0; i < length; i++) ...[
this[i],
if (i != length - 1) separator,
]
];
}
Iterable<T> whereFromIndex(bool test(T element), {int startIndex = 0}) {
final whereList = <T>[];
for (int i = startIndex; i < length; i++) {
final element = this[i];
if (test(element)) {
whereList.add(element);
}
}
return whereList;
}
bool containsWhere(bool test(T element)) {
for (var e in this) {
if (test(e)) {
return true;
}
}
return false;
}
}
Map<String, String> devToolsQueryParams(String url) {
// DevTools urls can have the form:
// http://localhost:123/?key=value
// http://localhost:123/#/?key=value
// http://localhost:123/#/page-id?key=value
// Since we just want the query params, we will modify the url to have an
// easy-to-parse form.
final modifiedUri = url.replaceFirst(RegExp(r'#\/(\w*)[?]'), '?');
final uri = Uri.parse(modifiedUri);
return uri.queryParameters;
}
/// Gets a VM Service URI from a query string.
///
/// We read from the 'uri' value if it exists; otherwise we create a uri from
/// the from 'port' and 'token' values.
Uri? getServiceUriFromQueryString(String? location) {
if (location == null) {
return null;
}
final queryParams = Uri.parse(location).queryParameters;
// First try to use uri.
if (queryParams['uri'] != null) {
final uri = Uri.tryParse(queryParams['uri']!);
// Lots of things are considered valid URIs (including empty strings
// and single letters) since they can be relative, so we need to do some
// extra checks.
if (uri != null &&
uri.isAbsolute &&
(uri.isScheme('ws') ||
uri.isScheme('wss') ||
uri.isScheme('http') ||
uri.isScheme('https') ||
uri.isScheme('sse') ||
uri.isScheme('sses'))) {
return uri;
}
}
// Otherwise try 'port', 'token', and 'host'.
final port = int.tryParse(queryParams['port'] ?? '');
final token = queryParams['token'];
final host = queryParams['host'] ?? 'localhost';
if (port != null) {
if (token == null) {
return Uri.parse('ws://$host:$port/ws');
} else {
return Uri.parse('ws://$host:$port/$token/ws');
}
}
return null;
}
double safePositiveDouble(double value) {
if (value.isNaN) return 0.0;
return max(value, 0.0);
}
/// Displays timestamp using locale's timezone HH:MM:SS, if isUtc is false.
/// @param isUTC - if true for testing, the UTC locale is used (instead of
/// the user's locale). Tests will then pass when run in any timezone. All
/// formatted timestamps are displayed using the UTC locale.
String prettyTimestamp(
int? timestamp, {
bool isUtc = false,
}) {
if (timestamp == null) return '';
final timestampDT = DateTime.fromMillisecondsSinceEpoch(
timestamp,
isUtc: isUtc,
);
return DateFormat.Hms().format(timestampDT); // HH:mm:ss
}
/// A [ChangeNotifier] that holds a list of data.
///
/// This class also exposes methods to interact with the data. By default,
/// listeners are notified whenever the data is modified, but notifying can be
/// optionally disabled.
class ListValueNotifier<T> extends ChangeNotifier
implements ValueListenable<List<T>> {
/// Creates a [ListValueNotifier] that wraps this value [_rawList].
ListValueNotifier(List<T> rawList) : _rawList = List<T>.from(rawList) {
_currentList = ImmutableList(_rawList);
}
List<T> _rawList;
late ImmutableList<T> _currentList;
@override
List<T> get value => _currentList;
@override
// This override is needed to change visibility of the method.
// ignore: unnecessary_overrides
void notifyListeners() {
super.notifyListeners();
}
void _listChanged() {
_currentList = ImmutableList(_rawList);
notifyListeners();
}
set last(T value) {
// TODO(jacobr): use a more sophisticated data structure such as
// https://en.wikipedia.org/wiki/Rope_(data_structure) to make last more
// efficient.
_rawList = _rawList.toList();
_rawList.last = value;
_listChanged();
}
/// Adds an element to the list and notifies listeners.
void add(T element) {
_rawList.add(element);
_listChanged();
}
/// Replaces all elements in the list and notifies listeners. It's preferred
/// to calling .clear() then .addAll(), because it only notifies listeners
/// once.
void replaceAll(Iterable<T> elements) {
_rawList = <T>[];
_rawList.addAll(elements);
_listChanged();
}
/// Adds elements to the list and notifies listeners.
void addAll(Iterable<T> elements) {
_rawList.addAll(elements);
_listChanged();
}
void removeAll(Iterable<T> elements) {
elements.forEach(_rawList.remove);
_listChanged();
}
/// Clears the list and notifies listeners.
void clear() {
_rawList = <T>[];
_listChanged();
}
/// Truncates to just the elements between [start] and [end].
///
/// If [end] is omitted, it defaults to the [length] of this list.
///
/// The `start` and `end` positions must satisfy the relations
/// 0 ≤ `start` ≤ `end` ≤ [length]
/// If `end` is equal to `start`, then the returned list is empty.
void trimToSublist(int start, [int? end]) {
// TODO(jacobr): use a more sophisticated data structure such as
// https://en.wikipedia.org/wiki/Rope_(data_structure) to make the
// implementation of this method more efficient.
_rawList = _rawList.sublist(start, end);
_listChanged();
}
/// Removes the first occurrence of [value] from this list.
///
/// Runtime is O(n).
bool remove(T value) {
final index = _rawList.indexOf(value);
if (index == -1) return false;
_rawList = _rawList.toList();
_rawList.removeAt(index);
_listChanged();
return true;
}
/// Removes a range of elements from the list.
///
/// https://api.flutter.dev/flutter/dart-core/List/removeRange.html
void removeRange(int start, int end) {
_rawList = _rawList.toList();
_rawList.removeRange(start, end);
_listChanged();
}
}
/// Wrapper for a list that prevents any modification of the list's content.
///
/// This class should only be used as part of [ListValueNotifier].
@visibleForTesting
class ImmutableList<T> with ListMixin<T> implements List<T> {
ImmutableList(this._rawList) : length = _rawList.length;
final List<T> _rawList;
@override
int length;
@override
T operator [](int index) {
if (index >= 0 && index < length) {
return _rawList[index];
} else {
throw Exception('Index out of range [0-${length - 1}]: $index');
}
}
@override
void operator []=(int index, T value) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void add(T element) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void addAll(Iterable<T> iterable) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
bool remove(Object? element) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
T removeAt(int index) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
T removeLast() {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void removeRange(int start, int end) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void removeWhere(bool test(T element)) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void retainWhere(bool test(T element)) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void insert(int index, T element) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void insertAll(int index, Iterable<T> iterable) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void clear() {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void fillRange(int start, int end, [T? fill]) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void setRange(int start, int end, Iterable<T> iterable, [int skipCount = 0]) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void replaceRange(int start, int end, Iterable<T> newContents) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void setAll(int index, Iterable<T> iterable) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void sort([int Function(T a, T b)? compare]) {
throw Exception('Cannot modify the content of ImmutableList');
}
@override
void shuffle([Random? random]) {
throw Exception('Cannot modify the content of ImmutableList');
}
}
extension BoolExtension on bool {
int boolCompare(bool other) {
if ((this && other) || (!this && !other)) return 0;
if (other) return 1;
return -1;
}
}
Future<T> whenValueNonNull<T>(ValueListenable<T> listenable) {
if (listenable.value != null) return Future.value(listenable.value);
final completer = Completer<T>();
void listener() {
final value = listenable.value;
if (value != null) {
completer.complete(value);
listenable.removeListener(listener);
}
}
listenable.addListener(listener);
return completer.future;
}
const connectToNewAppText = 'Connect to a new app';
/// Exception thrown when a request to process data has been cancelled in
/// favor of a new request.
class ProcessCancelledException implements Exception {}
extension UriExtension on Uri {
Uri copyWith({
String? scheme,
String? userInfo,
String? host,
int? port,
String? path,
Iterable<String>? pathSegments,
String? query,
Map<String, dynamic>? queryParameters,
String? fragment,
}) {
return Uri(
scheme: scheme ?? this.scheme,
userInfo: userInfo ?? this.userInfo,
host: host ?? this.host,
port: port ?? this.port,
pathSegments: pathSegments ?? this.pathSegments,
query: query ?? this.query,
queryParameters: queryParameters ?? this.queryParameters,
fragment: fragment ?? this.fragment,
);
}
}
Iterable<T> removeNullValues<T>(Iterable<T?> values) {
return values.whereType<T>();
}
bool isPrimativeInstanceKind(String? kind) {
return kind == InstanceKind.kBool ||
kind == InstanceKind.kDouble ||
kind == InstanceKind.kInt ||
kind == InstanceKind.kNull ||
kind == InstanceKind.kString;
}