| // Copyright (c) 2023, 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:math'; | |
| import 'package:collection/collection.dart'; | |
| import 'json_schema.dart'; | |
| import 'json_schema_extensions.dart'; | |
| /// Generates Dart classes from the Debug Adapter Protocol's JSON Schema. | |
| class CodeGenerator { | |
| /// Writes all required Dart classes for the supplied DAP [schema]. | |
| void writeAll(IndentableStringBuffer buffer, JsonSchema schema) { | |
| _writeDefinitionClasses(buffer, schema); | |
| buffer.writeln(); | |
| _writeBodyClasses(buffer, schema); | |
| buffer.writeln(); | |
| _writeEventTypeLookup(buffer, schema); | |
| buffer.writeln(); | |
| _writeCommandArgumentTypeLookup(buffer, schema); | |
| } | |
| /// Maps a name used in the DAP spec to a valid name for use in Dart. | |
| /// | |
| /// Reserved words like `default` will be mapped to a suitable alternative. | |
| /// Prefixed underscores are removed to avoid making things private. | |
| /// | |
| /// Underscores between words are swapped for camelCase. | |
| String _dartSafeName(String name) { | |
| const improvedName = { | |
| 'default': 'defaultValue', | |
| }; | |
| return improvedName[name] ?? | |
| // Some types are prefixed with _ in the spec but that will make them | |
| // private in Dart and inaccessible to the adapter so we strip it off. | |
| name | |
| .replaceAll(RegExp(r'^_+'), '') | |
| // Also replace any other underscores to make camelCase | |
| .replaceAllMapped(RegExp(r'_(.)'), (m) => m[1]!.toUpperCase()); | |
| } | |
| /// Re-wraps [lines] at [maxLength] to help keep comments for indented code | |
| /// within 80 characters. | |
| Iterable<String> _wrapLines(List<String> lines, int maxLength) sync* { | |
| lines = lines.map((l) => l.trimRight()).toList(); | |
| for (var line in lines) { | |
| while (true) { | |
| if (line.length <= maxLength || line.startsWith('-')) { | |
| yield line; | |
| break; | |
| } else { | |
| var lastSpace = line.lastIndexOf(' ', max(maxLength, 0)); | |
| // If there was no valid place to wrap, yield the whole string. | |
| if (lastSpace == -1) { | |
| yield line; | |
| break; | |
| } else { | |
| yield line.substring(0, lastSpace); | |
| line = line.substring(lastSpace + 1); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| /// For each Response/Event class in the spec, generate a specific class to | |
| /// represent its body. | |
| /// | |
| /// These classes are used to simplify sending responses/events from the | |
| /// Debug Adapters by avoiding the need to construct the entire response/event | |
| /// which requires additional fields (for example the corresponding requests | |
| /// id/command and sequences): | |
| /// | |
| /// this.sendResponse(FooBody(x: 1)) | |
| /// | |
| /// instead of | |
| /// | |
| /// this.sendResponse(Response( | |
| /// seq: seq++, | |
| /// request_seq: request.seq, | |
| /// command: request.command, | |
| /// body: { | |
| /// x: 1 | |
| /// ... | |
| /// } | |
| /// )) | |
| void _writeBodyClasses(IndentableStringBuffer buffer, JsonSchema schema) { | |
| for (final entry in schema.definitions.entries.sortedBy((e) => e.key)) { | |
| final name = entry.key; | |
| final type = entry.value; | |
| final baseType = type.baseType; | |
| if (baseType?.refName == 'Response' || baseType?.refName == 'Event') { | |
| final baseClass = baseType?.refName == 'Event' | |
| ? JsonType.named(schema, 'EventBody') | |
| : null; | |
| final classProperties = schema.propertiesFor(type); | |
| final bodyProperty = classProperties['body']; | |
| var bodyPropertyProperties = bodyProperty?.properties; | |
| _writeClass( | |
| buffer, | |
| bodyProperty ?? JsonType.empty(schema), | |
| '${name}Body', | |
| bodyPropertyProperties ?? {}, | |
| {}, | |
| baseClass, | |
| null, | |
| ); | |
| } | |
| } | |
| } | |
| /// Writes a `canParse` function for a DAP spec class. | |
| /// | |
| /// The function checks whether an Object? is a valid map that contains all | |
| /// required fields and matches the types of the spec class. | |
| /// | |
| /// This is used where the spec contains union classes and we need to decide | |
| /// which of the allowed types a given value is. | |
| void _writeCanParseMethod( | |
| IndentableStringBuffer buffer, | |
| JsonType type, | |
| Map<String, JsonType> properties, { | |
| required String? baseTypeRefName, | |
| }) { | |
| buffer | |
| ..writeIndentedln('static bool canParse(Object? obj) {') | |
| ..indent() | |
| ..writeIndentedln('if (obj is! Map<String, dynamic>) {') | |
| ..indent() | |
| ..writeIndentedln('return false;') | |
| ..outdent() | |
| ..writeIndentedln('}'); | |
| // In order to consider this valid for parsing, all fields that must not be | |
| // undefined must be present and also type check for the correct type. | |
| // Any fields that are optional but present, must still type check. | |
| for (final entry in properties.entries.sortedBy((e) => e.key)) { | |
| final propertyName = entry.key; | |
| final propertyType = entry.value; | |
| final isOptional = !type.requiresField(propertyName); | |
| if (propertyType.isAny && isOptional) { | |
| continue; | |
| } | |
| buffer.writeIndented('if ('); | |
| _writeTypeCheckCondition(buffer, propertyType, "obj['$propertyName']", | |
| isOptional: isOptional, invert: true); | |
| buffer | |
| ..writeln(') {') | |
| ..indent() | |
| ..writeIndentedln('return false;') | |
| ..outdent() | |
| ..writeIndentedln('}'); | |
| } | |
| buffer | |
| ..writeIndentedln( | |
| baseTypeRefName != null | |
| ? 'return $baseTypeRefName.canParse(obj);' | |
| : 'return true;', | |
| ) | |
| ..outdent() | |
| ..writeIndentedln('}'); | |
| } | |
| /// Writes the Dart class for [type]. | |
| void _writeClass( | |
| IndentableStringBuffer buffer, | |
| JsonType type, | |
| String name, | |
| Map<String, JsonType> classProperties, | |
| Map<String, JsonType> baseProperties, | |
| JsonType? baseType, | |
| JsonType? resolvedBaseType, { | |
| Map<String, String> additionalValues = const {}, | |
| }) { | |
| _writeTypeDescription(buffer, type); | |
| // Types that are just aliases to simple value types should be written as | |
| // typedefs. | |
| if (type.isSimpleValue) { | |
| buffer.writeln('typedef $name = ${type.asDartType()};'); | |
| return; | |
| } | |
| // Some properties are defined in both the base and the class, because the | |
| // type may be narrowed, but sometimes we only want those that are defined | |
| // only in this class. | |
| final classOnlyProperties = { | |
| for (final property in classProperties.entries) | |
| if (!baseProperties.containsKey(property.key)) | |
| property.key: property.value, | |
| }; | |
| buffer.write('class $name '); | |
| if (baseType != null) { | |
| buffer.write('extends ${baseType.refName} '); | |
| } | |
| buffer | |
| ..writeln('{') | |
| ..indent(); | |
| for (final val in additionalValues.entries) { | |
| buffer | |
| ..writeIndentedln('@override') | |
| ..writeIndentedln("final ${val.key} = '${val.value}';"); | |
| } | |
| _writeFields(buffer, type, classOnlyProperties); | |
| buffer.writeln(); | |
| _writeFromJsonStaticMethod(buffer, name); | |
| buffer.writeln(); | |
| _writeConstructor(buffer, name, type, classProperties, baseProperties, | |
| classOnlyProperties, | |
| baseType: resolvedBaseType); | |
| buffer.writeln(); | |
| _writeFromMapConstructor(buffer, name, type, classOnlyProperties, | |
| callSuper: resolvedBaseType != null); | |
| buffer.writeln(); | |
| _writeCanParseMethod(buffer, type, classProperties, | |
| baseTypeRefName: baseType?.refName); | |
| buffer.writeln(); | |
| _writeToJsonMethod(buffer, name, type, classOnlyProperties, | |
| callSuper: resolvedBaseType != null); | |
| buffer | |
| ..outdent() | |
| ..writeln('}') | |
| ..writeln(); | |
| } | |
| /// Write a map to look up the `command` for a given `RequestArguments` type | |
| /// to simplify sending requests back to the client: | |
| /// | |
| /// this.sendRequest(FooArguments(x: 1)) | |
| /// | |
| /// instead of | |
| /// | |
| /// this.sendRequest(Request( | |
| /// seq: seq++, | |
| /// command: request.command, | |
| /// arguments: { | |
| /// x: 1 | |
| /// ... | |
| /// } | |
| /// )) | |
| void _writeCommandArgumentTypeLookup( | |
| IndentableStringBuffer buffer, JsonSchema schema) { | |
| buffer | |
| ..writeln('const commandTypes = {') | |
| ..indent(); | |
| for (final entry in schema.definitions.entries.sortedBy((e) => e.key)) { | |
| final type = entry.value; | |
| final baseType = type.baseType; | |
| if (baseType?.refName == 'Request') { | |
| final classProperties = schema.propertiesFor(type); | |
| final argumentsProperty = classProperties['arguments']; | |
| final commandType = classProperties['command']?.literalValue; | |
| if (argumentsProperty?.dollarRef != null && commandType != null) { | |
| buffer.writeIndentedln( | |
| "${argumentsProperty!.refName}: '$commandType',"); | |
| } | |
| } | |
| } | |
| buffer | |
| ..writeln('};') | |
| ..outdent(); | |
| } | |
| /// Writes a constructor for [type]. | |
| /// | |
| /// The constructor will have named arguments for all fields, with those that | |
| /// are mandatory marked with `required`. | |
| void _writeConstructor( | |
| IndentableStringBuffer buffer, | |
| String name, | |
| JsonType type, | |
| Map<String, JsonType> classProperties, | |
| Map<String, JsonType> baseProperties, | |
| Map<String, JsonType> classOnlyProperties, { | |
| required JsonType? baseType, | |
| }) { | |
| buffer.writeIndented('$name('); | |
| if (classProperties.isNotEmpty || baseProperties.isNotEmpty) { | |
| buffer | |
| ..writeln('{') | |
| ..indent(); | |
| // Properties for this class are written as 'this.foo'. | |
| for (final entry in classOnlyProperties.entries.sortedBy((e) => e.key)) { | |
| final propertyName = entry.key; | |
| final fieldName = _dartSafeName(propertyName); | |
| final isOptional = !type.requiresField(propertyName); | |
| buffer.writeIndented(''); | |
| if (!isOptional) { | |
| buffer.write('required '); | |
| } | |
| buffer.writeln('this.$fieldName, '); | |
| } | |
| // Properties from the base class are super-parameters. | |
| for (final entry in baseProperties.entries.sortedBy((e) => e.key)) { | |
| final propertyName = entry.key; | |
| // If this field is defined by the class and the base, prefer the | |
| // class one as it may contain things like the literal values. | |
| final propertyType = classProperties[propertyName] ?? entry.value; | |
| final fieldName = _dartSafeName(propertyName); | |
| if (propertyType.literalValue != null) { | |
| continue; | |
| } | |
| final isOptional = !type.requiresField(propertyName); | |
| buffer.writeIndented(''); | |
| if (!isOptional) { | |
| buffer.write('required '); | |
| } | |
| buffer.writeln('super.$fieldName, '); | |
| } | |
| buffer | |
| ..outdent() | |
| ..writeIndented('}'); | |
| } | |
| buffer.write(')'); | |
| // We might still need an explicit super() call if we have literal values | |
| // to pass through. | |
| if (baseType != null) { | |
| final requiredExplicitSuperArgs = baseProperties.entries | |
| .map((entry) => | |
| MapEntry(entry.key, classProperties[entry.key]?.literalValue)) | |
| .where((entry) => entry.value != null) | |
| .toList(); | |
| if (requiredExplicitSuperArgs.isNotEmpty) { | |
| buffer | |
| ..write(': super(') | |
| ..writeln() | |
| ..indent(); | |
| for (final entry in requiredExplicitSuperArgs) { | |
| final name = entry.key; | |
| final value = entry.value; | |
| buffer.writeIndentedln('$name: \'$value\', '); | |
| } | |
| buffer | |
| ..outdent() | |
| ..writeIndented('') | |
| ..write(')'); | |
| } | |
| } | |
| buffer.writeln(';'); | |
| } | |
| /// Write a class for each item in the DAP spec. | |
| /// | |
| /// Skips over the Request and Event sub-classes, as they are handled by the | |
| /// simplified body classes written by [_writeBodyClasses]. Uses | |
| /// [RequestArguments] as the base class for all argument classes. | |
| void _writeDefinitionClasses( | |
| IndentableStringBuffer buffer, JsonSchema schema) { | |
| for (final entry in schema.definitions.entries.sortedBy((e) => e.key)) { | |
| final name = entry.key; | |
| final type = entry.value; | |
| var baseType = type.baseType; | |
| final resolvedBaseType = | |
| baseType != null ? schema.typeFor(baseType) : null; | |
| final classProperties = schema.propertiesFor(type, includeBase: false); | |
| final baseProperties = resolvedBaseType != null | |
| ? schema.propertiesFor(resolvedBaseType) | |
| : <String, JsonType>{}; | |
| // Skip creation of Request sub-classes, as we don't use these we just | |
| // pass the arguments into the method directly. | |
| if (name != 'Request' && name.endsWith('Request')) { | |
| continue; | |
| } | |
| // Skip creation of Event sub-classes, as we don't use these we just | |
| // pass the body into sendEvent directly. | |
| if (name != 'Event' && name.endsWith('Event')) { | |
| continue; | |
| } | |
| // Create a synthetic base class for arguments to provide type safety | |
| // for sending requests. | |
| if (baseType == null && name.endsWith('Arguments')) { | |
| baseType = JsonType.named(schema, 'RequestArguments'); | |
| } | |
| _writeClass( | |
| buffer, | |
| type, | |
| name, | |
| classProperties, | |
| baseProperties, | |
| baseType, | |
| resolvedBaseType, | |
| ); | |
| } | |
| } | |
| /// Writes a DartDoc comment, wrapped at 80 characters taking into account | |
| /// the indentation. | |
| void _writeDescription(IndentableStringBuffer buffer, String? description) { | |
| final maxLength = 80 - buffer.totalIndent - 4; | |
| if (description != null) { | |
| for (final line in _wrapLines(description.split('\n'), maxLength)) { | |
| buffer.writeIndentedln('/// $line'); | |
| } | |
| } | |
| } | |
| /// Write a map to look up the `event` for a given `EventBody` type | |
| /// to simplify sending events back to the client: | |
| /// | |
| /// this.sendEvent(FooEvent(x: 1)) | |
| /// | |
| /// instead of | |
| /// | |
| /// this.sendEvent(Event( | |
| /// seq: seq++, | |
| /// event: 'FooEvent', | |
| /// arguments: { | |
| /// x: 1 | |
| /// ... | |
| /// } | |
| /// )) | |
| void _writeEventTypeLookup(IndentableStringBuffer buffer, JsonSchema schema) { | |
| buffer | |
| ..writeln('const eventTypes = {') | |
| ..indent(); | |
| for (final entry in schema.definitions.entries.sortedBy((e) => e.key)) { | |
| final name = entry.key; | |
| final type = entry.value; | |
| final baseType = type.baseType; | |
| if (baseType?.refName == 'Event') { | |
| final classProperties = schema.propertiesFor(type); | |
| final eventType = classProperties['event']!.literalValue; | |
| buffer.writeIndentedln("${name}Body: '$eventType',"); | |
| } | |
| } | |
| buffer | |
| ..writeln('};') | |
| ..outdent(); | |
| } | |
| /// Writes Dart fields for [properties], taking into account whether they are | |
| /// required for [type]. | |
| void _writeFields(IndentableStringBuffer buffer, JsonType type, | |
| Map<String, JsonType> properties) { | |
| for (final entry in properties.entries.sortedBy((e) => e.key)) { | |
| final propertyName = entry.key; | |
| final fieldName = _dartSafeName(propertyName); | |
| final propertyType = entry.value; | |
| final isOptional = !type.requiresField(propertyName); | |
| final dartType = propertyType.asDartType(isOptional: isOptional); | |
| _writeDescription(buffer, propertyType.description); | |
| buffer.writeIndentedln('final $dartType $fieldName;'); | |
| } | |
| } | |
| /// Writes an expression to deserialize a [valueCode]. | |
| /// | |
| /// If [type] represents a spec type, it's `fromJson` function will be called. | |
| /// If [type] is a [List], it will be mapped over this function again. | |
| /// If [type] is an union, the appropriate `canParse` functions will be used to | |
| /// determine which `fromJson` function to call. | |
| void _writeFromJsonExpression( | |
| IndentableStringBuffer buffer, JsonType type, String valueCode, | |
| {bool isOptional = false}) { | |
| final baseType = type.aliasFor ?? type; | |
| final dartType = type.asDartType(isOptional: isOptional); | |
| final dartTypeNotNullable = type.asDartType(); | |
| final nullOp = isOptional ? '?' : ''; | |
| if (baseType.isAny || baseType.isSimple) { | |
| buffer.write(valueCode); | |
| if (dartType != 'Object?') { | |
| buffer.write(' as $dartType'); | |
| } | |
| } else if (type.isList) { | |
| buffer.write('($valueCode as List$nullOp)$nullOp.map((item) => '); | |
| _writeFromJsonExpression(buffer, type.items!, 'item'); | |
| buffer.write(').toList()'); | |
| } else if (type.isUnion) { | |
| final types = type.unionTypes; | |
| // Write a check against each type, e.g.: | |
| // x is y ? new Either.tx(x) : (...) | |
| for (var i = 0; i < types.length; i++) { | |
| final isLast = i == types.length - 1; | |
| // For the last item, if we're optional we won't wrap if in a check, as | |
| // the constructor will only be called if canParse() returned true, so | |
| // it'll the only remaining option. | |
| if (!isLast || isOptional) { | |
| _writeTypeCheckCondition(buffer, types[i], valueCode, | |
| isOptional: false); | |
| buffer.write(' ? '); | |
| } | |
| buffer.write('$dartTypeNotNullable.t${i + 1}('); | |
| _writeFromJsonExpression(buffer, types[i], valueCode); | |
| buffer.write(')'); | |
| if (!isLast) { | |
| buffer.write(' : '); | |
| } else if (isLast && isOptional) { | |
| buffer.write(' : null'); | |
| } | |
| } | |
| } else if (type.isSpecType) { | |
| if (isOptional) { | |
| buffer.write('$valueCode == null ? null : '); | |
| } | |
| buffer.write( | |
| '$dartTypeNotNullable.fromJson($valueCode as Map<String, Object?>)'); | |
| } else { | |
| throw 'Unable to type check $valueCode against $type'; | |
| } | |
| } | |
| /// Writes a static `fromJson` method that converts an object into a spec type | |
| /// by calling its fromMap constructor. | |
| /// | |
| /// This is a helper method used as a tear-off since the constructor cannot be. | |
| void _writeFromJsonStaticMethod( | |
| IndentableStringBuffer buffer, | |
| String name, | |
| ) => | |
| buffer.writeIndentedln( | |
| 'static $name fromJson(Map<String, Object?> obj) => $name.fromMap(obj);'); | |
| /// Writes a fromMap constructor to construct an object from a JSON map. | |
| void _writeFromMapConstructor( | |
| IndentableStringBuffer buffer, | |
| String name, | |
| JsonType type, | |
| Map<String, JsonType> properties, { | |
| bool callSuper = false, | |
| }) { | |
| buffer | |
| ..writeIndented('$name.fromMap(') | |
| ..write(callSuper ? 'super.obj' : 'Map<String, Object?> obj') | |
| ..write(')'); | |
| if (properties.isNotEmpty || callSuper) { | |
| buffer | |
| ..writeln(':') | |
| ..indent(); | |
| var isFirst = true; | |
| for (final entry in properties.entries.sortedBy((e) => e.key)) { | |
| if (isFirst) { | |
| isFirst = false; | |
| } else { | |
| buffer.writeln(','); | |
| } | |
| final propertyName = entry.key; | |
| final fieldName = _dartSafeName(propertyName); | |
| final propertyType = entry.value; | |
| final isOptional = !type.requiresField(propertyName); | |
| buffer.writeIndented('$fieldName = '); | |
| _writeFromJsonExpression(buffer, propertyType, "obj['$propertyName']", | |
| isOptional: isOptional); | |
| } | |
| if (callSuper) { | |
| if (!isFirst) { | |
| buffer.writeln(','); | |
| } | |
| buffer.writeIndented('super.fromMap()'); | |
| } | |
| buffer.outdent(); | |
| } | |
| buffer.writeln(';'); | |
| } | |
| /// Writes a toJson method to construct a JSON map for this class, recursively | |
| /// calling through base classes. | |
| void _writeToJsonMethod( | |
| IndentableStringBuffer buffer, | |
| String name, | |
| JsonType type, | |
| Map<String, JsonType> properties, { | |
| bool callSuper = false, | |
| }) { | |
| if (callSuper) { | |
| buffer.writeIndentedln('@override'); | |
| } | |
| buffer | |
| ..writeIndentedln('Map<String, Object?> toJson() => {') | |
| ..indent(); | |
| if (callSuper) { | |
| buffer.writeIndentedln('...super.toJson(),'); | |
| } | |
| for (final entry in properties.entries.sortedBy((e) => e.key)) { | |
| final propertyName = entry.key; | |
| final fieldName = _dartSafeName(propertyName); | |
| final isOptional = !type.requiresField(propertyName); | |
| buffer.writeIndented(''); | |
| if (isOptional) { | |
| buffer.write('if ($fieldName != null) '); | |
| } | |
| buffer.writeln("'$propertyName': $fieldName, "); | |
| } | |
| buffer | |
| ..outdent() | |
| ..writeIndentedln('};'); | |
| } | |
| /// Writes an expression that checks whether [valueCode] represents a [type]. | |
| void _writeTypeCheckCondition( | |
| IndentableStringBuffer buffer, JsonType type, String valueCode, | |
| {required bool isOptional, bool invert = false}) { | |
| final baseType = type.aliasFor ?? type; | |
| final dartType = type.asDartType(isOptional: isOptional); | |
| // When the expression is inverted, invert the operators so the generated | |
| // code is easier to read. | |
| final opBang = invert ? '!' : ''; | |
| final opTrue = invert ? 'false' : 'true'; | |
| final opIs = invert ? 'is!' : 'is'; | |
| final opEquals = invert ? '!=' : '=='; | |
| final opAnd = invert ? '||' : '&&'; | |
| final opOr = invert ? '&&' : '||'; | |
| final opEvery = invert ? 'any' : 'every'; | |
| if (baseType.isAny) { | |
| buffer.write(opTrue); | |
| } else if (dartType == 'Null') { | |
| buffer.write('$valueCode $opEquals null'); | |
| } else if (baseType.isSimple) { | |
| buffer.write('$valueCode $opIs $dartType'); | |
| } else if (type.isList) { | |
| buffer.write('($valueCode $opIs List'); | |
| buffer.write(' $opAnd ($valueCode.$opEvery((item) => '); | |
| _writeTypeCheckCondition(buffer, type.items!, 'item', | |
| isOptional: false, invert: invert); | |
| buffer.write('))'); | |
| buffer.write(')'); | |
| } else if (type.isUnion) { | |
| final types = type.unionTypes; | |
| // To type check a union, we recursively check against each of its types. | |
| buffer.write('('); | |
| for (var i = 0; i < types.length; i++) { | |
| if (i != 0) { | |
| buffer.write(' $opOr '); | |
| } | |
| _writeTypeCheckCondition(buffer, types[i], valueCode, | |
| isOptional: false, invert: invert); | |
| } | |
| if (isOptional) { | |
| buffer.write(' $opOr $valueCode $opEquals null'); | |
| } | |
| buffer.write(')'); | |
| } else if (type.isSpecType) { | |
| buffer.write('$opBang${type.asDartType()}.canParse($valueCode)'); | |
| } else { | |
| throw 'Unable to type check $valueCode against $type'; | |
| } | |
| } | |
| /// Writes the description for [type], looking at the base type from the | |
| /// DAP spec if necessary. | |
| void _writeTypeDescription(IndentableStringBuffer buffer, JsonType type) { | |
| // In the DAP spec, many of the descriptions are on one of the allOf types | |
| // rather than the type itself. | |
| final description = type.description ?? | |
| type.allOf | |
| ?.firstWhereOrNull((element) => element.description != null) | |
| ?.description; | |
| _writeDescription(buffer, description); | |
| } | |
| } | |
| /// A [StringBuffer] with support for indenting. | |
| class IndentableStringBuffer extends StringBuffer { | |
| int _indentLevel = 0; | |
| final int _indentSpaces = 2; | |
| int get totalIndent => _indentLevel * _indentSpaces; | |
| String get _indentString => ' ' * totalIndent; | |
| void indent() => _indentLevel++; | |
| void outdent() => _indentLevel--; | |
| void writeIndented(Object obj) { | |
| write(_indentString); | |
| write(obj); | |
| } | |
| void writeIndentedln(Object obj) { | |
| write(_indentString); | |
| writeln(obj); | |
| } | |
| } |