blob: ebdc7454ef4b9042d66f7d9cd616cc20ed4b106b [file] [log] [blame]
Devon Carew67046f92016-02-20 22:00:11 -08001// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
Greg Spencer081d2a72018-10-10 18:17:56 -07006import 'dart:math' show Random, max;
Devon Carew4e10bf52016-02-26 17:53:41 -08007
Devon Carew20e83e32017-04-11 07:57:18 -07008import 'package:intl/intl.dart';
Devon Carew4e10bf52016-02-26 17:53:41 -08009
Jonah Williams91fd89e2019-01-25 16:16:26 -080010import '../convert.dart';
Devon Carewa7c3bf42017-08-01 15:29:54 -070011import '../globals.dart';
xster66ed8de2017-04-27 15:28:15 -070012import 'context.dart';
Todd Volkert8bb27032017-01-06 16:51:44 -080013import 'file_system.dart';
Greg Spencer081d2a72018-10-10 18:17:56 -070014import 'io.dart' as io;
Todd Volkert417c2f22017-01-25 16:06:41 -080015import 'platform.dart';
Greg Spencer081d2a72018-10-10 18:17:56 -070016import 'terminal.dart';
Todd Volkert8bb27032017-01-06 16:51:44 -080017
Alexandre Ardhuineda03e22018-08-02 12:02:32 +020018const BotDetector _kBotDetector = BotDetector();
jcollins-gca677012018-02-21 09:54:07 -080019
20class BotDetector {
21 const BotDetector();
22
23 bool get isRunningOnBot {
Todd Volkert1b3fc532019-06-10 16:32:08 -070024 if (
25 // Explicitly stated to not be a bot.
26 platform.environment['BOT'] == 'false'
27
28 // Set by the IDEs to the IDE name, so a strong signal that this is not a bot.
29 || platform.environment.containsKey('FLUTTER_HOST')
30 ) {
31 return false;
32 }
33
34 return platform.environment['BOT'] == 'true'
35
Greg Spencere60087a2018-08-07 13:41:33 -070036 // https://docs.travis-ci.com/user/environment-variables/#Default-Environment-Variables
Ian Hicksond1cc8b62018-06-14 12:30:20 -070037 || platform.environment['TRAVIS'] == 'true'
38 || platform.environment['CONTINUOUS_INTEGRATION'] == 'true'
39 || platform.environment.containsKey('CI') // Travis and AppVeyor
jcollins-gca677012018-02-21 09:54:07 -080040
Greg Spencere60087a2018-08-07 13:41:33 -070041 // https://www.appveyor.com/docs/environment-variables/
Ian Hicksond1cc8b62018-06-14 12:30:20 -070042 || platform.environment.containsKey('APPVEYOR')
jcollins-gca677012018-02-21 09:54:07 -080043
Greg Spencere60087a2018-08-07 13:41:33 -070044 // https://cirrus-ci.org/guide/writing-tasks/#environment-variables
45 || platform.environment.containsKey('CIRRUS_CI')
46
47 // https://docs.aws.amazon.com/codebuild/latest/userguide/build-env-ref-env-vars.html
Zachary Andersonfa65ddf2019-07-16 09:48:49 -070048 || (platform.environment.containsKey('AWS_REGION') &&
49 platform.environment.containsKey('CODEBUILD_INITIATOR'))
jcollins-gca677012018-02-21 09:54:07 -080050
Greg Spencere60087a2018-08-07 13:41:33 -070051 // https://wiki.jenkins.io/display/JENKINS/Building+a+software+project#Buildingasoftwareproject-belowJenkinsSetEnvironmentVariables
Ian Hicksond1cc8b62018-06-14 12:30:20 -070052 || platform.environment.containsKey('JENKINS_URL')
jcollins-gca677012018-02-21 09:54:07 -080053
Greg Spencere60087a2018-08-07 13:41:33 -070054 // Properties on Flutter's Chrome Infra bots.
Ian Hicksond1cc8b62018-06-14 12:30:20 -070055 || platform.environment['CHROME_HEADLESS'] == '1'
Todd Volkert1b3fc532019-06-10 16:32:08 -070056 || platform.environment.containsKey('BUILDBOT_BUILDERNAME')
57 || platform.environment.containsKey('SWARMING_TASK_ID');
jcollins-gca677012018-02-21 09:54:07 -080058 }
59}
60
Devon Carewbd564a02016-05-05 19:51:22 -070061bool get isRunningOnBot {
Jonah Williams0acd3e62019-04-25 15:51:08 -070062 final BotDetector botDetector = context.get<BotDetector>() ?? _kBotDetector;
Todd Volkert37ffb2d2018-02-21 15:01:46 -080063 return botDetector.isRunningOnBot;
Devon Carewbd564a02016-05-05 19:51:22 -070064}
65
Devon Carew4c569192016-03-02 11:22:19 -080066/// Convert `foo_bar` to `fooBar`.
67String camelCase(String str) {
68 int index = str.indexOf('_');
69 while (index != -1 && index < str.length - 2) {
70 str = str.substring(0, index) +
71 str.substring(index + 1, index + 2).toUpperCase() +
72 str.substring(index + 2);
73 index = str.indexOf('_');
74 }
75 return str;
76}
77
Alexandre Ardhuind927c932018-09-12 08:29:29 +020078final RegExp _upperRegex = RegExp(r'[A-Z]');
Stanislav Baranov393f9272018-08-16 08:43:41 -070079
80/// Convert `fooBar` to `foo_bar`.
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +010081String snakeCase(String str, [ String sep = '_' ]) {
Stanislav Baranov393f9272018-08-16 08:43:41 -070082 return str.replaceAllMapped(_upperRegex,
83 (Match m) => '${m.start == 0 ? '' : sep}${m[0].toLowerCase()}');
84}
85
Devon Carew9cfa9662016-05-26 09:14:51 -070086String toTitleCase(String str) {
87 if (str.isEmpty)
88 return str;
89 return str.substring(0, 1).toUpperCase() + str.substring(1);
90}
91
Devon Carew4c569192016-03-02 11:22:19 -080092/// Return the plural of the given word (`cat(s)`).
93String pluralize(String word, int count) => count == 1 ? word : word + 's';
94
Devon Carewf132aca2016-04-15 21:08:03 -070095/// Return the name of an enum item.
96String getEnumName(dynamic enumItem) {
Chris Bracken7a093162017-03-03 17:50:46 -080097 final String name = '$enumItem';
98 final int index = name.indexOf('.');
Devon Carewf132aca2016-04-15 21:08:03 -070099 return index == -1 ? name : name.substring(index + 1);
100}
101
Devon Carew15b9e1d2016-03-25 16:04:22 -0700102File getUniqueFile(Directory dir, String baseName, String ext) {
Chris Bracken7a093162017-03-03 17:50:46 -0800103 final FileSystem fs = dir.fileSystem;
Devon Carew15b9e1d2016-03-25 16:04:22 -0700104 int i = 1;
105
106 while (true) {
Chris Bracken7a093162017-03-03 17:50:46 -0800107 final String name = '${baseName}_${i.toString().padLeft(2, '0')}.$ext';
108 final File file = fs.file(fs.path.join(dir.path, name));
Devon Carew15b9e1d2016-03-25 16:04:22 -0700109 if (!file.existsSync())
110 return file;
111 i++;
112 }
113}
114
Devon Carew40c0d6e2016-05-12 18:15:23 -0700115String toPrettyJson(Object jsonable) {
Alexandre Ardhuin28881392017-02-20 23:07:16 +0100116 return const JsonEncoder.withIndent(' ').convert(jsonable) + '\n';
Devon Carew40c0d6e2016-05-12 18:15:23 -0700117}
118
Devon Carew7c478372016-05-29 15:07:41 -0700119/// Return a String - with units - for the size in MB of the given number of bytes.
120String getSizeAsMB(int bytesLength) {
121 return '${(bytesLength / (1024 * 1024)).toStringAsFixed(1)}MB';
122}
123
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200124final NumberFormat kSecondsFormat = NumberFormat('0.0');
125final NumberFormat kMillisecondsFormat = NumberFormat.decimalPattern();
Devon Carew20e83e32017-04-11 07:57:18 -0700126
127String getElapsedAsSeconds(Duration duration) {
Jason Simmons466d1542018-03-12 11:06:32 -0700128 final double seconds = duration.inMilliseconds / Duration.millisecondsPerSecond;
Devon Carew20e83e32017-04-11 07:57:18 -0700129 return '${kSecondsFormat.format(seconds)}s';
130}
131
132String getElapsedAsMilliseconds(Duration duration) {
133 return '${kMillisecondsFormat.format(duration.inMilliseconds)}ms';
134}
John McCutchanb314fa52016-08-10 14:02:44 -0700135
Devon Carew3ba17132016-06-07 12:13:35 -0700136/// Return a relative path if [fullPath] is contained by the cwd, else return an
137/// absolute path.
138String getDisplayPath(String fullPath) {
Chris Bracken7a093162017-03-03 17:50:46 -0800139 final String cwd = fs.currentDirectory.path + fs.path.separator;
Alexandre Ardhuinc02b6a82018-02-02 23:27:29 +0100140 return fullPath.startsWith(cwd) ? fullPath.substring(cwd.length) : fullPath;
Devon Carew3ba17132016-06-07 12:13:35 -0700141}
142
Devon Carew67046f92016-02-20 22:00:11 -0800143/// A class to maintain a list of items, fire events when items are added or
144/// removed, and calculate a diff of changes when a new list of items is
145/// available.
146class ItemListNotifier<T> {
147 ItemListNotifier() {
Phil Quitslund802eca22019-03-06 11:05:16 -0800148 _items = <T>{};
Devon Carew67046f92016-02-20 22:00:11 -0800149 }
150
151 ItemListNotifier.from(List<T> items) {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200152 _items = Set<T>.from(items);
Devon Carew67046f92016-02-20 22:00:11 -0800153 }
154
155 Set<T> _items;
156
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200157 final StreamController<T> _addedController = StreamController<T>.broadcast();
158 final StreamController<T> _removedController = StreamController<T>.broadcast();
Devon Carew67046f92016-02-20 22:00:11 -0800159
160 Stream<T> get onAdded => _addedController.stream;
161 Stream<T> get onRemoved => _removedController.stream;
162
163 List<T> get items => _items.toList();
164
165 void updateWithNewList(List<T> updatedList) {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200166 final Set<T> updatedSet = Set<T>.from(updatedList);
Devon Carew67046f92016-02-20 22:00:11 -0800167
Chris Bracken7a093162017-03-03 17:50:46 -0800168 final Set<T> addedItems = updatedSet.difference(_items);
169 final Set<T> removedItems = _items.difference(updatedSet);
Devon Carew67046f92016-02-20 22:00:11 -0800170
171 _items = updatedSet;
172
Alexandre Ardhuin28366002017-10-25 08:25:44 +0200173 addedItems.forEach(_addedController.add);
174 removedItems.forEach(_removedController.add);
Devon Carew67046f92016-02-20 22:00:11 -0800175 }
176
177 /// Close the streams.
178 void dispose() {
179 _addedController.close();
180 _removedController.close();
181 }
182}
Devon Carew57b76a02016-07-19 20:00:02 -0700183
184class SettingsFile {
Jakob Andersen6b541372017-05-05 14:53:51 +0200185 SettingsFile();
186
Devon Carew57b76a02016-07-19 20:00:02 -0700187 SettingsFile.parse(String contents) {
188 for (String line in contents.split('\n')) {
189 line = line.trim();
190 if (line.startsWith('#') || line.isEmpty)
191 continue;
Chris Bracken7a093162017-03-03 17:50:46 -0800192 final int index = line.indexOf('=');
Devon Carew57b76a02016-07-19 20:00:02 -0700193 if (index != -1)
194 values[line.substring(0, index)] = line.substring(index + 1);
195 }
196 }
197
198 factory SettingsFile.parseFromFile(File file) {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200199 return SettingsFile.parse(file.readAsStringSync());
Devon Carew57b76a02016-07-19 20:00:02 -0700200 }
201
202 final Map<String, String> values = <String, String>{};
203
204 void writeContents(File file) {
Stanislav Baranovf6c14762018-12-18 10:39:27 -0800205 file.parent.createSync(recursive: true);
Alexandre Ardhuinf62afdc2018-10-01 21:29:08 +0200206 file.writeAsStringSync(values.keys.map<String>((String key) {
Devon Carew57b76a02016-07-19 20:00:02 -0700207 return '$key=${values[key]}';
208 }).join('\n'));
209 }
210}
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700211
212/// A UUID generator. This will generate unique IDs in the format:
213///
214/// f47ac10b-58cc-4372-a567-0e02b2c3d479
215///
Greg Spencer0259be92017-11-17 10:05:21 -0800216/// The generated UUIDs are 128 bit numbers encoded in a specific string format.
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700217///
218/// For more information, see
219/// http://en.wikipedia.org/wiki/Universally_unique_identifier.
220class Uuid {
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200221 final Random _random = Random();
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700222
Greg Spencer0259be92017-11-17 10:05:21 -0800223 /// Generate a version 4 (random) UUID. This is a UUID scheme that only uses
224 /// random numbers as the source of the generated UUID.
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700225 String generateV4() {
226 // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12.
Chris Bracken7a093162017-03-03 17:50:46 -0800227 final int special = 8 + _random.nextInt(4);
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700228
229 return
230 '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-'
231 '${_bitsDigits(16, 4)}-'
232 '4${_bitsDigits(12, 3)}-'
Alexandre Ardhuinc02b6a82018-02-02 23:27:29 +0100233 '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-'
Devon Carewd9bbd2f2016-09-28 11:18:05 -0700234 '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}';
235 }
236
237 String _bitsDigits(int bitCount, int digitCount) =>
238 _printDigits(_generateBits(bitCount), digitCount);
239
240 int _generateBits(int bitCount) => _random.nextInt(1 << bitCount);
241
242 String _printDigits(int value, int count) =>
243 value.toRadixString(16).padLeft(count, '0');
244}
xster66ed8de2017-04-27 15:28:15 -0700245
Devon Carew9d9836f2018-07-09 12:22:46 -0700246/// Given a data structure which is a Map of String to dynamic values, return
247/// the same structure (`Map<String, dynamic>`) with the correct runtime types.
248Map<String, dynamic> castStringKeyedMap(dynamic untyped) {
249 final Map<dynamic, dynamic> map = untyped;
250 return map.cast<String, dynamic>();
251}
252
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200253typedef AsyncCallback = Future<void> Function();
Devon Carewa7c3bf42017-08-01 15:29:54 -0700254
255/// A [Timer] inspired class that:
256/// - has a different initial value for the first callback delay
257/// - waits for a callback to be complete before it starts the next timer
258class Poller {
Alexandre Ardhuin09276be2018-06-05 08:50:40 +0200259 Poller(this.callback, this.pollingInterval, { this.initialDelay = Duration.zero }) {
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200260 Future<void>.delayed(initialDelay, _handleCallback);
Devon Carewa7c3bf42017-08-01 15:29:54 -0700261 }
262
263 final AsyncCallback callback;
264 final Duration initialDelay;
265 final Duration pollingInterval;
266
Chris Bracken156b4222019-05-24 19:13:02 -0700267 bool _canceled = false;
Devon Carewa7c3bf42017-08-01 15:29:54 -0700268 Timer _timer;
269
Alexandre Ardhuin2d3ff102018-10-05 07:54:56 +0200270 Future<void> _handleCallback() async {
Chris Bracken156b4222019-05-24 19:13:02 -0700271 if (_canceled)
Devon Carewa7c3bf42017-08-01 15:29:54 -0700272 return;
273
274 try {
275 await callback();
276 } catch (error) {
277 printTrace('Error from poller: $error');
278 }
279
Chris Bracken156b4222019-05-24 19:13:02 -0700280 if (!_canceled)
Alexandre Ardhuind927c932018-09-12 08:29:29 +0200281 _timer = Timer(pollingInterval, _handleCallback);
Devon Carewa7c3bf42017-08-01 15:29:54 -0700282 }
283
284 /// Cancels the poller.
285 void cancel() {
Chris Bracken156b4222019-05-24 19:13:02 -0700286 _canceled = true;
Devon Carewa7c3bf42017-08-01 15:29:54 -0700287 _timer?.cancel();
288 _timer = null;
289 }
290}
xster987b2052017-09-20 16:25:16 -0700291
292/// Returns a [Future] that completes when all given [Future]s complete.
293///
294/// Uses [Future.wait] but removes null elements from the provided
295/// `futures` iterable first.
296///
297/// The returned [Future<List>] will be shorter than the given `futures` if
298/// it contains nulls.
299Future<List<T>> waitGroup<T>(Iterable<Future<T>> futures) {
300 return Future.wait<T>(futures.where((Future<T> future) => future != null));
301}
Greg Spencer081d2a72018-10-10 18:17:56 -0700302/// The terminal width used by the [wrapText] function if there is no terminal
303/// attached to [io.Stdio], --wrap is on, and --wrap-columns was not specified.
304const int kDefaultTerminalColumns = 100;
305
306/// Smallest column that will be used for text wrapping. If the requested column
307/// width is smaller than this, then this is what will be used.
308const int kMinColumnWidth = 10;
309
310/// Wraps a block of text into lines no longer than [columnWidth].
311///
312/// Tries to split at whitespace, but if that's not good enough to keep it
Greg Spencer4559ae12018-10-30 16:00:50 -0700313/// under the limit, then it splits in the middle of a word. If [columnWidth] is
314/// smaller than 10 columns, will wrap at 10 columns.
Greg Spencer081d2a72018-10-10 18:17:56 -0700315///
316/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
317/// in the input, and will indent wrapped lines that same amount, adding
318/// [indent] spaces in addition to any existing indent.
319///
320/// If [hangingIndent] is supplied, then that many additional spaces will be
321/// added to each line, except for the first line. The [hangingIndent] is added
322/// to the specified [indent], if any. This is useful for wrapping
323/// text with a heading prefix (e.g. "Usage: "):
324///
325/// ```dart
326/// String prefix = "Usage: ";
327/// print(prefix + wrapText(invocation, indent: 2, hangingIndent: prefix.length, columnWidth: 40));
328/// ```
329///
330/// yields:
331/// ```
332/// Usage: app main_command <subcommand>
333/// [arguments]
334/// ```
335///
336/// If [columnWidth] is not specified, then the column width will be the
337/// [outputPreferences.wrapColumn], which is set with the --wrap-column option.
338///
339/// If [outputPreferences.wrapText] is false, then the text will be returned
Greg Spencer4559ae12018-10-30 16:00:50 -0700340/// unchanged. If [shouldWrap] is specified, then it overrides the
341/// [outputPreferences.wrapText] setting.
Greg Spencer081d2a72018-10-10 18:17:56 -0700342///
343/// The [indent] and [hangingIndent] must be smaller than [columnWidth] when
344/// added together.
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100345String wrapText(String text, { int columnWidth, int hangingIndent, int indent, bool shouldWrap }) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700346 if (text == null || text.isEmpty) {
347 return '';
348 }
349 indent ??= 0;
350 columnWidth ??= outputPreferences.wrapColumn;
351 columnWidth -= indent;
352 assert(columnWidth >= 0);
353
354 hangingIndent ??= 0;
355 final List<String> splitText = text.split('\n');
356 final List<String> result = <String>[];
357 for (String line in splitText) {
358 String trimmedText = line.trimLeft();
359 final String leadingWhitespace = line.substring(0, line.length - trimmedText.length);
360 List<String> notIndented;
361 if (hangingIndent != 0) {
362 // When we have a hanging indent, we want to wrap the first line at one
363 // width, and the rest at another (offset by hangingIndent), so we wrap
364 // them twice and recombine.
365 final List<String> firstLineWrap = _wrapTextAsLines(
366 trimmedText,
367 columnWidth: columnWidth - leadingWhitespace.length,
Greg Spencer4559ae12018-10-30 16:00:50 -0700368 shouldWrap: shouldWrap,
Greg Spencer081d2a72018-10-10 18:17:56 -0700369 );
370 notIndented = <String>[firstLineWrap.removeAt(0)];
371 trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
372 if (firstLineWrap.isNotEmpty) {
373 notIndented.addAll(_wrapTextAsLines(
374 trimmedText,
375 columnWidth: columnWidth - leadingWhitespace.length - hangingIndent,
Greg Spencer4559ae12018-10-30 16:00:50 -0700376 shouldWrap: shouldWrap,
Greg Spencer081d2a72018-10-10 18:17:56 -0700377 ));
378 }
379 } else {
380 notIndented = _wrapTextAsLines(
381 trimmedText,
382 columnWidth: columnWidth - leadingWhitespace.length,
Greg Spencer4559ae12018-10-30 16:00:50 -0700383 shouldWrap: shouldWrap,
Greg Spencer081d2a72018-10-10 18:17:56 -0700384 );
385 }
386 String hangingIndentString;
387 final String indentString = ' ' * indent;
388 result.addAll(notIndented.map(
389 (String line) {
390 // Don't return any lines with just whitespace on them.
391 if (line.isEmpty) {
392 return '';
393 }
394 final String result = '$indentString${hangingIndentString ?? ''}$leadingWhitespace$line';
395 hangingIndentString ??= ' ' * hangingIndent;
396 return result;
397 },
398 ));
399 }
400 return result.join('\n');
401}
402
Danny Tuppenyaa83f772018-10-24 07:21:36 +0100403void writePidFile(String pidFile) {
404 if (pidFile != null) {
405 // Write our pid to the file.
406 fs.file(pidFile).writeAsStringSync(io.pid.toString());
407 }
408}
409
Greg Spencer081d2a72018-10-10 18:17:56 -0700410// Used to represent a run of ANSI control sequences next to a visible
411// character.
412class _AnsiRun {
413 _AnsiRun(this.original, this.character);
414
415 String original;
416 String character;
417}
418
419/// Wraps a block of text into lines no longer than [columnWidth], starting at the
420/// [start] column, and returning the result as a list of strings.
421///
422/// Tries to split at whitespace, but if that's not good enough to keep it
423/// under the limit, then splits in the middle of a word. Preserves embedded
424/// newlines, but not indentation (it trims whitespace from each line).
425///
426/// If [columnWidth] is not specified, then the column width will be the width of the
427/// terminal window by default. If the stdout is not a terminal window, then the
428/// default will be [outputPreferences.wrapColumn].
429///
430/// If [outputPreferences.wrapText] is false, then the text will be returned
Greg Spencer4559ae12018-10-30 16:00:50 -0700431/// simply split at the newlines, but not wrapped. If [shouldWrap] is specified,
432/// then it overrides the [outputPreferences.wrapText] setting.
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100433List<String> _wrapTextAsLines(String text, { int start = 0, int columnWidth, bool shouldWrap }) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700434 if (text == null || text.isEmpty) {
435 return <String>[''];
436 }
437 assert(columnWidth != null);
438 assert(columnWidth >= 0);
439 assert(start >= 0);
Greg Spencer4559ae12018-10-30 16:00:50 -0700440 shouldWrap ??= outputPreferences.wrapText;
Greg Spencer081d2a72018-10-10 18:17:56 -0700441
442 /// Returns true if the code unit at [index] in [text] is a whitespace
443 /// character.
444 ///
445 /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
446 bool isWhitespace(_AnsiRun run) {
447 final int rune = run.character.isNotEmpty ? run.character.codeUnitAt(0) : 0x0;
448 return rune >= 0x0009 && rune <= 0x000D ||
449 rune == 0x0020 ||
450 rune == 0x0085 ||
451 rune == 0x1680 ||
452 rune == 0x180E ||
453 rune >= 0x2000 && rune <= 0x200A ||
454 rune == 0x2028 ||
455 rune == 0x2029 ||
456 rune == 0x202F ||
457 rune == 0x205F ||
458 rune == 0x3000 ||
459 rune == 0xFEFF;
460 }
461
462 // Splits a string so that the resulting list has the same number of elements
463 // as there are visible characters in the string, but elements may include one
464 // or more adjacent ANSI sequences. Joining the list elements again will
465 // reconstitute the original string. This is useful for manipulating "visible"
466 // characters in the presence of ANSI control codes.
467 List<_AnsiRun> splitWithCodes(String input) {
468 final RegExp characterOrCode = RegExp('(\u001b\[[0-9;]*m|.)', multiLine: true);
469 List<_AnsiRun> result = <_AnsiRun>[];
470 final StringBuffer current = StringBuffer();
471 for (Match match in characterOrCode.allMatches(input)) {
472 current.write(match[0]);
473 if (match[0].length < 4) {
474 // This is a regular character, write it out.
475 result.add(_AnsiRun(current.toString(), match[0]));
476 current.clear();
477 }
478 }
479 // If there's something accumulated, then it must be an ANSI sequence, so
480 // add it to the end of the last entry so that we don't lose it.
481 if (current.isNotEmpty) {
482 if (result.isNotEmpty) {
483 result.last.original += current.toString();
484 } else {
485 // If there is nothing in the string besides control codes, then just
486 // return them as the only entry.
487 result = <_AnsiRun>[_AnsiRun(current.toString(), '')];
488 }
489 }
490 return result;
491 }
492
Alexandre Ardhuin5169ab52019-02-21 09:27:07 +0100493 String joinRun(List<_AnsiRun> list, int start, [ int end ]) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700494 return list.sublist(start, end).map<String>((_AnsiRun run) => run.original).join().trim();
495 }
496
497 final List<String> result = <String>[];
498 final int effectiveLength = max(columnWidth - start, kMinColumnWidth);
499 for (String line in text.split('\n')) {
500 // If the line is short enough, even with ANSI codes, then we can just add
501 // add it and move on.
Greg Spencer4559ae12018-10-30 16:00:50 -0700502 if (line.length <= effectiveLength || !shouldWrap) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700503 result.add(line);
504 continue;
505 }
506 final List<_AnsiRun> splitLine = splitWithCodes(line);
507 if (splitLine.length <= effectiveLength) {
508 result.add(line);
509 continue;
510 }
511
512 int currentLineStart = 0;
513 int lastWhitespace;
514 // Find the start of the current line.
515 for (int index = 0; index < splitLine.length; ++index) {
516 if (splitLine[index].character.isNotEmpty && isWhitespace(splitLine[index])) {
517 lastWhitespace = index;
518 }
519
520 if (index - currentLineStart >= effectiveLength) {
521 // Back up to the last whitespace, unless there wasn't any, in which
522 // case we just split where we are.
523 if (lastWhitespace != null) {
524 index = lastWhitespace;
525 }
526
527 result.add(joinRun(splitLine, currentLineStart, index));
528
529 // Skip any intervening whitespace.
Jonah Williamsd80999d2018-11-01 20:04:52 -0700530 while (index < splitLine.length && isWhitespace(splitLine[index])) {
Greg Spencer081d2a72018-10-10 18:17:56 -0700531 index++;
532 }
533
534 currentLineStart = index;
535 lastWhitespace = null;
536 }
537 }
538 result.add(joinRun(splitLine, currentLineStart));
539 }
540 return result;
541}