diff --git a/.dart_tool/extension_discovery/README.md b/.dart_tool/extension_discovery/README.md deleted file mode 100644 index 9dc6757..0000000 --- a/.dart_tool/extension_discovery/README.md +++ /dev/null @@ -1,31 +0,0 @@ -Extension Discovery Cache -========================= - -This folder is used by `package:extension_discovery` to cache lists of -packages that contains extensions for other packages. - -DO NOT USE THIS FOLDER ----------------------- - - * Do not read (or rely) the contents of this folder. - * Do write to this folder. - -If you're interested in the lists of extensions stored in this folder use the -API offered by package `extension_discovery` to get this information. - -If this package doesn't work for your use-case, then don't try to read the -contents of this folder. It may change, and will not remain stable. - -Use package `extension_discovery` ---------------------------------- - -If you want to access information from this folder. - -Feel free to delete this folder -------------------------------- - -Files in this folder act as a cache, and the cache is discarded if the files -are older than the modification time of `.dart_tool/package_config.json`. - -Hence, it should never be necessary to clear this cache manually, if you find a -need to do please file a bug. diff --git a/.dart_tool/extension_discovery/vs_code.json b/.dart_tool/extension_discovery/vs_code.json deleted file mode 100644 index b3de1c0..0000000 --- a/.dart_tool/extension_discovery/vs_code.json +++ /dev/null @@ -1 +0,0 @@ -{"version":2,"entries":[{"package":"dart_safe_data_class_test","rootUri":"../","packageUri":"lib/"}]} \ No newline at end of file diff --git a/.dart_tool/package_config.json b/.dart_tool/package_config.json deleted file mode 100644 index 8fb1a1c..0000000 --- a/.dart_tool/package_config.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "configVersion": 2, - "packages": [ - { - "name": "async", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/async-2.11.0", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "boolean_selector", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1", - "packageUri": "lib/", - "languageVersion": "2.17" - }, - { - "name": "characters", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/characters-1.3.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "clock", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/clock-1.1.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "collection", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/collection-1.18.0", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "fake_async", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/fake_async-1.3.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "flutter", - "rootUri": "file:///Users/art/flutter/packages/flutter", - "packageUri": "lib/", - "languageVersion": "3.2" - }, - { - "name": "flutter_test", - "rootUri": "file:///Users/art/flutter/packages/flutter_test", - "packageUri": "lib/", - "languageVersion": "3.2" - }, - { - "name": "matcher", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/matcher-0.12.16", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "material_color_utilities", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/material_color_utilities-0.5.0", - "packageUri": "lib/", - "languageVersion": "2.17" - }, - { - "name": "meta", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/meta-1.10.0", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "path", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/path-1.8.3", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "sky_engine", - "rootUri": "file:///Users/art/flutter/bin/cache/pkg/sky_engine", - "packageUri": "lib/", - "languageVersion": "3.2" - }, - { - "name": "source_span", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/source_span-1.10.0", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "stack_trace", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/stack_trace-1.11.1", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "stream_channel", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/stream_channel-2.1.2", - "packageUri": "lib/", - "languageVersion": "2.19" - }, - { - "name": "string_scanner", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/string_scanner-1.2.0", - "packageUri": "lib/", - "languageVersion": "2.18" - }, - { - "name": "term_glyph", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/term_glyph-1.2.1", - "packageUri": "lib/", - "languageVersion": "2.12" - }, - { - "name": "test_api", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/test_api-0.6.1", - "packageUri": "lib/", - "languageVersion": "3.0" - }, - { - "name": "vector_math", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/vector_math-2.1.4", - "packageUri": "lib/", - "languageVersion": "2.14" - }, - { - "name": "web", - "rootUri": "file:///Users/art/.pub-cache/hosted/pub.dev/web-0.3.0", - "packageUri": "lib/", - "languageVersion": "3.2" - }, - { - "name": "dart_safe_data_class_test", - "rootUri": "../", - "packageUri": "lib/", - "languageVersion": "3.2" - } - ], - "generated": "2024-02-09T10:28:04.966730Z", - "generator": "pub", - "generatorVersion": "3.2.6" -} diff --git a/.dart_tool/package_config_subset b/.dart_tool/package_config_subset deleted file mode 100644 index 6e3dd56..0000000 --- a/.dart_tool/package_config_subset +++ /dev/null @@ -1,89 +0,0 @@ -async -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/async-2.11.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/async-2.11.0/lib/ -boolean_selector -2.17 -file:///Users/art/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib/ -characters -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/characters-1.3.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/characters-1.3.0/lib/ -clock -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/clock-1.1.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/clock-1.1.1/lib/ -collection -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/collection-1.18.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/collection-1.18.0/lib/ -fake_async -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/fake_async-1.3.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/fake_async-1.3.1/lib/ -matcher -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/matcher-0.12.16/ -file:///Users/art/.pub-cache/hosted/pub.dev/matcher-0.12.16/lib/ -material_color_utilities -2.17 -file:///Users/art/.pub-cache/hosted/pub.dev/material_color_utilities-0.5.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/material_color_utilities-0.5.0/lib/ -meta -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/meta-1.10.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/meta-1.10.0/lib/ -path -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/path-1.8.3/ -file:///Users/art/.pub-cache/hosted/pub.dev/path-1.8.3/lib/ -source_span -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/source_span-1.10.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/source_span-1.10.0/lib/ -stack_trace -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib/ -stream_channel -2.19 -file:///Users/art/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/ -file:///Users/art/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/lib/ -string_scanner -2.18 -file:///Users/art/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/lib/ -term_glyph -2.12 -file:///Users/art/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/lib/ -test_api -3.0 -file:///Users/art/.pub-cache/hosted/pub.dev/test_api-0.6.1/ -file:///Users/art/.pub-cache/hosted/pub.dev/test_api-0.6.1/lib/ -vector_math -2.14 -file:///Users/art/.pub-cache/hosted/pub.dev/vector_math-2.1.4/ -file:///Users/art/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/ -web -3.2 -file:///Users/art/.pub-cache/hosted/pub.dev/web-0.3.0/ -file:///Users/art/.pub-cache/hosted/pub.dev/web-0.3.0/lib/ -dart_safe_data_class_test -3.2 -file:///Users/art/Documents/GitHub/dart-data-class-tools/ -file:///Users/art/Documents/GitHub/dart-data-class-tools/lib/ -sky_engine -3.2 -file:///Users/art/flutter/bin/cache/pkg/sky_engine/ -file:///Users/art/flutter/bin/cache/pkg/sky_engine/lib/ -flutter -3.2 -file:///Users/art/flutter/packages/flutter/ -file:///Users/art/flutter/packages/flutter/lib/ -flutter_test -3.2 -file:///Users/art/flutter/packages/flutter_test/ -file:///Users/art/flutter/packages/flutter_test/lib/ -2 diff --git a/.dart_tool/version b/.dart_tool/version deleted file mode 100644 index eea6645..0000000 --- a/.dart_tool/version +++ /dev/null @@ -1 +0,0 @@ -3.16.9 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 30bc162..ab097de 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -/node_modules \ No newline at end of file +/node_modules +.dart_tool \ No newline at end of file diff --git a/README.md b/README.md index 05dd8cf..931bccf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Dart Safe Data Class Generator +# Dart Safe Data Class Generator (shodev.live) Create dart data classes easily, fast and without writing yourself or running code generation. diff --git a/assets/gif_from_json.gif b/assets/gif_from_json.gif deleted file mode 100644 index 3587a21..0000000 Binary files a/assets/gif_from_json.gif and /dev/null differ diff --git a/dart-safe-data-class-shodev-0.12.0.vsix b/dart-safe-data-class-shodev-0.12.0.vsix new file mode 100644 index 0000000..5975ab0 Binary files /dev/null and b/dart-safe-data-class-shodev-0.12.0.vsix differ diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..36339d4 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +.pubspec.lock +.metadata + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..1b7a4e3 --- /dev/null +++ b/example/README.md @@ -0,0 +1,3 @@ +# example + +A new Flutter project. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..a725658 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World!'), + ), + ), + ); + } +} diff --git a/test.dart b/example/lib/test.dart similarity index 76% rename from test.dart rename to example/lib/test.dart index a983912..73508e4 100644 --- a/test.dart +++ b/example/lib/test.dart @@ -1,6 +1,8 @@ // Generated by Dart Safe Data Class Generator. * Change this header on extension settings * -// ignore_for_file: type=lint -import 'package:flutter/foundation.dart'; +// ignore_for_file: type=lint, deprecated_member_use +import 'dart:convert'; + +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; enum Status { active, inactive } @@ -28,80 +30,26 @@ class Types { final List? noOthers; const Types({ this.age, - this.radius = 0, - this.height = 0.0, + required this.radius, + required this.height, this.isPremium, required this.status, this.lastStatus, - this.name = '', - this.names = const [], + required this.name, + required this.names, this.info, - this.objects = const [], + required this.objects, required this.color, this.colors, this.date, - this.dates = const [], + required this.dates, required this.icon, this.icons, this.another, - this.anothers = const [], - this.noOthers = const [], + required this.anothers, + this.noOthers, }); - @override - String toString() { - return 'Types(age: $age, radius: $radius, height: $height, isPremium: $isPremium, status: $status, lastStatus: $lastStatus, name: $name, names: $names, info: $info, objects: $objects, color: $color, colors: $colors, date: $date, dates: $dates, icon: $icon, icons: $icons, another: $another, anothers: $anothers, noOthers: $noOthers)'; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is Types && - other.age == age && - other.radius == radius && - other.height == height && - other.isPremium == isPremium && - other.status == status && - other.lastStatus == lastStatus && - other.name == name && - listEquals(other.names, names) && - mapEquals(other.info, info) && - listEquals(other.objects, objects) && - other.color == color && - listEquals(other.colors, colors) && - other.date == date && - listEquals(other.dates, dates) && - other.icon == icon && - listEquals(other.icons, icons) && - other.another == another && - listEquals(other.anothers, anothers) && - listEquals(other.noOthers, noOthers); - } - - @override - int get hashCode { - return age.hashCode ^ - radius.hashCode ^ - height.hashCode ^ - isPremium.hashCode ^ - status.hashCode ^ - lastStatus.hashCode ^ - name.hashCode ^ - names.hashCode ^ - info.hashCode ^ - objects.hashCode ^ - color.hashCode ^ - colors.hashCode ^ - date.hashCode ^ - dates.hashCode ^ - icon.hashCode ^ - icons.hashCode ^ - another.hashCode ^ - anothers.hashCode ^ - noOthers.hashCode; - } - Types copyWith({ int? age, num? radius, @@ -152,8 +100,8 @@ class Types { 'radius': radius, 'height': height, 'is_premium': isPremium, - 'status': status.index, - 'last_status': lastStatus?.index, + 'status': status.name, + 'last_status': lastStatus?.name, 'name': name, 'names': names, 'info': info, @@ -179,9 +127,9 @@ class Types { radius: cast('radius'), height: cast('height').toDouble(), isPremium: cast('is_premium'), - status: Status.values[cast('status').toInt()], + status: Status.values.byName(cast('status')), lastStatus: map['last_status'] != null - ? Status.values[cast('last_status').toInt()] + ? Status.values.byName(cast('last_status')) : null, name: cast('name'), names: List.from(cast('names')), @@ -208,11 +156,91 @@ class Types { anothers: List.from(cast('anothers') .map((x) => AnotherClass.fromMap(Map.from(x as Map)))), noOthers: map['no_others'] != null - ? List.from(cast('noOthers') + ? List.from(cast('no_others') .map((x) => AnotherClass.fromMap(Map.from(x as Map)))) : null, ); } + + String toJson() => json.encode(toMap()); + + factory Types.fromJson(String source) => + Types.fromMap(json.decode(source) as Map); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + final collectionEquals = const DeepCollectionEquality().equals; + + return other is Types && + other.age == age && + other.radius == radius && + other.height == height && + other.isPremium == isPremium && + other.status == status && + other.lastStatus == lastStatus && + other.name == name && + collectionEquals(other.names, names) && + collectionEquals(other.info, info) && + collectionEquals(other.objects, objects) && + other.color == color && + collectionEquals(other.colors, colors) && + other.date == date && + collectionEquals(other.dates, dates) && + other.icon == icon && + collectionEquals(other.icons, icons) && + other.another == another && + collectionEquals(other.anothers, anothers) && + collectionEquals(other.noOthers, noOthers); + } + + @override + int get hashCode { + return age.hashCode ^ + radius.hashCode ^ + height.hashCode ^ + isPremium.hashCode ^ + status.hashCode ^ + lastStatus.hashCode ^ + name.hashCode ^ + names.hashCode ^ + info.hashCode ^ + objects.hashCode ^ + color.hashCode ^ + colors.hashCode ^ + date.hashCode ^ + dates.hashCode ^ + icon.hashCode ^ + icons.hashCode ^ + another.hashCode ^ + anothers.hashCode ^ + noOthers.hashCode; + } + + @override + String toString() { + return '''Types( + age: $age, + radius: $radius, + height: $height, + isPremium: $isPremium, + status: $status, + lastStatus: $lastStatus, + name: $name, + names: $names, + info: $info, + objects: $objects, + color: $color, + colors: $colors, + date: $date, + dates: $dates, + icon: $icon, + icons: $icons, + another: $another, + anothers: $anothers, + noOthers: $noOthers, + )'''; + } } @immutable @@ -241,12 +269,19 @@ class AnotherClass { ? map[k] as T : throw ArgumentError.value(map[k], k, '$T ← ${map[k].runtimeType}'); return AnotherClass( - id: cast('id') ?? '', + id: cast('id'), ); } + String toJson() => json.encode(toMap()); + + factory AnotherClass.fromJson(String source) => + AnotherClass.fromMap(json.decode(source) as Map); + @override - String toString() => 'AnotherClass(id: $id)'; + String toString() => '''AnotherClass( + id: $id, + )'''; @override bool operator ==(Object other) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..c7d2fff --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,19 @@ +name: example +description: "A new Flutter project." +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ^3.6.1 + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true diff --git a/package.json b/package.json index 0668c5c..2136e15 100644 --- a/package.json +++ b/package.json @@ -1,331 +1,331 @@ { - "name": "dart-safe-data-class", - "displayName": "Dart Safe Data Class Generator", - "description": "Create dart data classes easily, with type safety and no boilerplate or code generation.", - "publisher": "ArthurMiranda", - "version": "0.12.0", - "engines": { - "vscode": "^1.65.0" - }, - "repository": { - "type": "git", - "url": "https://github.com/arthurbcd/dart-data-class-tools.git" - }, - "icon": "assets/icon.png", - "keywords": [ - "dart safe data class generator", - "dart data class generator", - "dart safe data class", - "dart data class", - "dart safe data", - "dart data", - "dart safe", - "dart", - "data safe class generator", - "data class generator" - ], - "author": { - "name": "arthurbcd" - }, - "categories": [ - "Programming Languages", - "Other" - ], - "activationEvents": [ - "onLanguage:dart", - "onCommand:dart_data_class.generate.from_props", - "onCommand:dart_data_class.generate.from_json" - ], - "main": "./src/extension.js", - "contributes": { - "commands": [ - { - "title": "Dart Safe Data Class Generator: Generate from class properties", - "command": "dart_data_class.generate.from_props" - }, - { - "title": "Dart Safe Data Class Generator: Generate from JSON", - "command": "dart_data_class.generate.from_json" - } - ], - "configuration": [ - { - "title": "Dart Safe Data Class Generator configuration", - "properties": { - "dart-data-class-generator.custom.argumentError": { - "type": "string", - "default": "throw ArgumentError.value(map[k], k, '$T ← ${map[k].runtimeType}');", - "description": "The error that is thrown on `cast()` invalid argument. Use `k` for key, `map[k]` for value and T for argument Type." - }, - "dart-data-class-generator.custom.headerLines": { - "type": "array", - "items": { - "type": "string" - }, - "default": [ - "// Generated by Dart Safe Data Class Generator. * Change this header on extension settings *", - "// ignore_for_file: type=lint" - ], - "description": "Custom header lines to be added at the top of the generated file." - }, - "dart-data-class-generator.custom.types": { - "type": "array", - "description": "Custom directives for specific types. Define each type with `type` for the type name, `fromMap` for the fromMap function, and `toMap` for the toMap function.", - "default": [ - { - "type": "DateTime", - "fromMap": "DateTime.parse(String)", - "toMap": "toIso8601String()" - }, - { - "type": "Color", - "fromMap": "Color(int)", - "toMap": "value" - }, - { - "type": "IconData", - "fromMap": "IconData(int)", - "toMap": "codePoint" - }, - { - "type": "Timestamp", - "fromMap": "", - "toMap": "" - } - ], - "items": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "The data type name." - }, - "fromMap": { - "type": "string", - "description": "How to convert in fromMap. Ex: DateTime.parse(String)" - }, - "toMap": { - "type": "string", - "description": "How to convert in toMap. Ex: toIso8601String()" - } - }, - "required": [ - "type", - "fromMap", - "toMap" - ] - } - }, - "dart-data-class-generator.json.enum_format": { - "type": "string", - "enum": [ - "byName", - "byIndex" - ], - "default": "byName", - "description": "Whether to use parse using Enum.values.byName or Enum.values[index]." - }, - "dart-data-class-generator.json.key_format": { - "type": "string", - "enum": [ - "variable", - "camelCase", - "snake_case" - ], - "default": "variable", - "description": "Whether to use snake_case or camelCase for the json keys." - }, - "dart-data-class-generator.json.separate": { - "type": "string", - "enum": [ - "ask", - "separate", - "current_file" - ], - "default": "ask", - "description": "Whether to separate a JSON into multiple files, when the JSON contains nested objects. ask: choose manually every time, separate: always separate into multiple files, current_file: always insert all classes into the current file." - }, - "dart-data-class-generator.strict_numbers": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, `int` and `double` will use isA(...) instead of isA(...).toInt() sintax." - }, - "dart-data-class-generator.quick_fixes": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, enables quick fixes to quickly generate data classes or specific methods only." - }, - "dart-data-class-generator.fromMap.default_values": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, checks if a field is null when deserializing and provides a non-null default value." - }, - "dart-data-class-generator.constructor.default_values": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, generates default values for the constructor." - }, - "dart-data-class-generator.constructor.immutable": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, adds @immutable annotation on the generated class. Note: This forces all fields to be final, and all constructor to be const." - }, - "dart-data-class-generator.constructor.required": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, generates the required annotation to every constructor parameter. Note: The generator wont generate default values for the constructor if enabled!" - }, - "dart-data-class-generator.override.manual": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, asks, when overriding a class (running the command on an existing class), for every single function/constructor that needs to be changed whether the generator should override the function or not. This allows you to preserve custom changes you made to the function/constructor that would be otherwise overwritten by the generator." - }, - "dart-data-class-generator.constructor.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a constructor for a data class." - }, - "dart-data-class-generator.copyWith.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a copyWith function for a data class." - }, - "dart-data-class-generator.toMap.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a toMap function for a data class." - }, - "dart-data-class-generator.fromMap.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a fromMap function for a data class." - }, - "dart-data-class-generator.toJson.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a toJson function for a data class." - }, - "dart-data-class-generator.fromJson.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a fromJson function for a data class." - }, - "dart-data-class-generator.toString.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a toString function for a data class." - }, - "dart-data-class-generator.equality.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates an override of the == (equals) operator for a data class." - }, - "dart-data-class-generator.hashCode.enabled": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": true, - "description": "If true, generates a hashCode function for a data class." - }, - "dart-data-class-generator.hashCode.use_jenkins": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, uses the Jenkins SMI hash function instead of bitwise operator from dart:ui." - }, - "dart-data-class-generator.useEquatable": { - "type": "boolean", - "enum": [ - true, - false - ], - "default": false, - "description": "If true, uses equatable for value equality and hashcode." - } - } - } - ] - }, - "scripts": { - "test": "node ./test/runTest.js" - }, - "devDependencies": { - "@types/glob": "^7.1.1", - "@types/mocha": "^5.2.6", - "@types/node": "^10.12.21", - "@types/vscode": "^1.37.0", - "eslint": "^5.13.0", - "glob": "^7.1.4", - "mocha": "^6.1.4", - "typescript": "^3.3.1", - "vscode-test": "^1.0.2" - }, - "dependencies": { - "vsce": "^2.5.1" - } + "name": "dart-safe-data-class-shodev", + "displayName": "Dart Safe Data Class Generator (shodev.live)", + "description": "Create dart data classes easily, with type safety and no boilerplate or code generation.", + "publisher": "yelmuratoff", + "version": "0.12.1", + "engines": { + "vscode": "^1.65.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/arthurbcd/dart-data-class-tools.git" + }, + "icon": "assets/icon.png", + "keywords": [ + "dart safe data class generator", + "dart data class generator", + "dart safe data class", + "dart data class", + "dart safe data", + "dart data", + "dart safe", + "dart", + "data safe class generator", + "data class generator" + ], + "author": { + "name": "yelmuratoff" + }, + "categories": [ + "Programming Languages", + "Other" + ], + "activationEvents": [ + "onLanguage:dart", + "onCommand:dart_data_class.generate.from_props", + "onCommand:dart_data_class.generate.from_json" + ], + "main": "./src/extension.js", + "contributes": { + "commands": [ + { + "title": "Dart Safe Data Class Generator: Generate from class properties", + "command": "dart_data_class.generate.from_props" + }, + { + "title": "Dart Safe Data Class Generator: Generate from JSON", + "command": "dart_data_class.generate.from_json" + } + ], + "configuration": [ + { + "title": "Dart Safe Data Class Generator configuration", + "properties": { + "dart-data-class-generator.custom.argumentError": { + "type": "string", + "default": "throw ArgumentError.value(map[k], k, '$T ← ${map[k].runtimeType}');", + "description": "The error that is thrown on `cast()` invalid argument. Use `k` for key, `map[k]` for value and T for argument Type." + }, + "dart-data-class-generator.custom.headerLines": { + "type": "array", + "items": { + "type": "string" + }, + "default": [ + "// Generated by Dart Safe Data Class Generator. * Change this header on extension settings *", + "// ignore_for_file: type=lint" + ], + "description": "Custom header lines to be added at the top of the generated file." + }, + "dart-data-class-generator.custom.types": { + "type": "array", + "description": "Custom directives for specific types. Define each type with `type` for the type name, `fromMap` for the fromMap function, and `toMap` for the toMap function.", + "default": [ + { + "type": "DateTime", + "fromMap": "DateTime.parse(String)", + "toMap": "toIso8601String()" + }, + { + "type": "Color", + "fromMap": "Color(int)", + "toMap": "value" + }, + { + "type": "IconData", + "fromMap": "IconData(int)", + "toMap": "codePoint" + }, + { + "type": "Timestamp", + "fromMap": "", + "toMap": "" + } + ], + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The data type name." + }, + "fromMap": { + "type": "string", + "description": "How to convert in fromMap. Ex: DateTime.parse(String)" + }, + "toMap": { + "type": "string", + "description": "How to convert in toMap. Ex: toIso8601String()" + } + }, + "required": [ + "type", + "fromMap", + "toMap" + ] + } + }, + "dart-data-class-generator.json.enum_format": { + "type": "string", + "enum": [ + "byName", + "byIndex" + ], + "default": "byName", + "description": "Whether to use parse using Enum.values.byName or Enum.values[index]." + }, + "dart-data-class-generator.json.key_format": { + "type": "string", + "enum": [ + "variable", + "camelCase", + "snake_case" + ], + "default": "variable", + "description": "Whether to use snake_case or camelCase for the json keys." + }, + "dart-data-class-generator.json.separate": { + "type": "string", + "enum": [ + "ask", + "separate", + "current_file" + ], + "default": "ask", + "description": "Whether to separate a JSON into multiple files, when the JSON contains nested objects. ask: choose manually every time, separate: always separate into multiple files, current_file: always insert all classes into the current file." + }, + "dart-data-class-generator.strict_numbers": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, `int` and `double` will use isA(...) instead of isA(...).toInt() sintax." + }, + "dart-data-class-generator.quick_fixes": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, enables quick fixes to quickly generate data classes or specific methods only." + }, + "dart-data-class-generator.fromMap.default_values": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, checks if a field is null when deserializing and provides a non-null default value." + }, + "dart-data-class-generator.constructor.default_values": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, generates default values for the constructor." + }, + "dart-data-class-generator.constructor.immutable": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, adds @immutable annotation on the generated class. Note: This forces all fields to be final, and all constructor to be const." + }, + "dart-data-class-generator.constructor.required": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, generates the required annotation to every constructor parameter. Note: The generator wont generate default values for the constructor if enabled!" + }, + "dart-data-class-generator.override.manual": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, asks, when overriding a class (running the command on an existing class), for every single function/constructor that needs to be changed whether the generator should override the function or not. This allows you to preserve custom changes you made to the function/constructor that would be otherwise overwritten by the generator." + }, + "dart-data-class-generator.constructor.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a constructor for a data class." + }, + "dart-data-class-generator.copyWith.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a copyWith function for a data class." + }, + "dart-data-class-generator.toMap.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a toMap function for a data class." + }, + "dart-data-class-generator.fromMap.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a fromMap function for a data class." + }, + "dart-data-class-generator.toJson.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a toJson function for a data class." + }, + "dart-data-class-generator.fromJson.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a fromJson function for a data class." + }, + "dart-data-class-generator.toString.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a toString function for a data class." + }, + "dart-data-class-generator.equality.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates an override of the == (equals) operator for a data class." + }, + "dart-data-class-generator.hashCode.enabled": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": true, + "description": "If true, generates a hashCode function for a data class." + }, + "dart-data-class-generator.hashCode.use_jenkins": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, uses the Jenkins SMI hash function instead of bitwise operator from dart:ui." + }, + "dart-data-class-generator.useEquatable": { + "type": "boolean", + "enum": [ + true, + false + ], + "default": false, + "description": "If true, uses equatable for value equality and hashcode." + } + } + } + ] + }, + "scripts": { + "test": "node ./test/runTest.js" + }, + "devDependencies": { + "@types/glob": "^7.1.1", + "@types/mocha": "^5.2.6", + "@types/node": "^10.12.21", + "@types/vscode": "^1.37.0", + "eslint": "^5.13.0", + "glob": "^7.1.4", + "mocha": "^6.1.4", + "typescript": "^3.3.1", + "vscode-test": "^1.0.2" + }, + "dependencies": { + "vsce": "^2.5.1" + } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index 982ce15..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,164 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.dev" - source: hosted - version: "1.18.0" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" - url: "https://pub.dev" - source: hosted - version: "0.5.0" - meta: - dependency: transitive - description: - name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e - url: "https://pub.dev" - source: hosted - version: "1.10.0" - path: - dependency: transitive - description: - name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" - url: "https://pub.dev" - source: hosted - version: "1.8.3" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.dev" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.dev" - source: hosted - version: "0.6.1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - web: - dependency: transitive - description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 - url: "https://pub.dev" - source: hosted - version: "0.3.0" -sdks: - dart: ">=3.2.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100644 index 9d35130..0000000 --- a/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: dart_safe_data_class_test -description: test -publish_to: 'none' -version: 0.1.0 - -environment: - sdk: '>=3.2.0 <4.0.0' - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: \ No newline at end of file diff --git a/src/extension.js b/src/extension.js index 118dd84..c5c2edb 100644 --- a/src/extension.js +++ b/src/extension.js @@ -1,2384 +1,2628 @@ -const vscode = require('vscode'); -const fs = require('fs'); -const path = require('path'); +const vscode = require("vscode"); +const fs = require("fs"); +const path = require("path"); -var projectName = ''; +var projectName = ""; var isFlutter = false; /** * @param {vscode.ExtensionContext} context */ function activate(context) { - context.subscriptions.push( - vscode.commands.registerCommand( - 'dart_data_class.generate.from_props', - generateDataClass - ) - ); - - context.subscriptions.push( - vscode.commands.registerCommand( - 'dart_data_class.generate.from_json', - generateJsonDataClass - ) - ); - - context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ - language: 'dart', - scheme: 'file' - }, new DataClassCodeActions(), { - providedCodeActionKinds: [ - vscode.CodeActionKind.QuickFix - ], - })); - - findProjectName(); + context.subscriptions.push( + vscode.commands.registerCommand( + "dart_data_class.generate.from_props", + generateDataClass + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "dart_data_class.generate.from_json", + generateJsonDataClass + ) + ); + + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { + language: "dart", + scheme: "file", + }, + new DataClassCodeActions(), + { + providedCodeActionKinds: [vscode.CodeActionKind.QuickFix], + } + ) + ); + + findProjectName(); } async function findProjectName() { - const pubspecs = await vscode.workspace.findFiles('pubspec.yaml'); + const pubspecs = await vscode.workspace.findFiles("pubspec.yaml"); - if (pubspecs != null && pubspecs.length > 0) { - const pubspec = pubspecs[0]; - const content = fs.readFileSync(pubspec.fsPath, 'utf8'); + if (pubspecs != null && pubspecs.length > 0) { + const pubspec = pubspecs[0]; + const content = fs.readFileSync(pubspec.fsPath, "utf8"); - if (content != null && content.includes('name: ')) { - isFlutter = content.includes('flutter:') && content.includes('sdk: flutter'); + if (content != null && content.includes("name: ")) { + isFlutter = + content.includes("flutter:") && content.includes("sdk: flutter"); - for (const line of content.split('\n')) { - if (line.startsWith('name: ')) { - projectName = line.replace('name:', '').trim(); - break; - } - } + for (const line of content.split("\n")) { + if (line.startsWith("name: ")) { + projectName = line.replace("name:", "").trim(); + break; } + } } + } } async function generateJsonDataClass() { - let langId = getLangId(); + let langId = getLangId(); - if (langId == 'dart') { - let document = getDocText(); + if (langId == "dart") { + let document = getDocText(); - const name = await vscode.window.showInputBox({ - placeHolder: 'Please type in a class name.' - }); + const name = await vscode.window.showInputBox({ + placeHolder: "Please type in a class name.", + }); - if (name == null || name.length == 0) { - return; - } + if (name == null || name.length == 0) { + return; + } - let reader = new JsonReader(document, name); - let separate = true; - - if (await reader.error == null) { - if (reader.files.length >= 2) { - const setting = readSetting('json.separate'); - - if (setting == 'ask') { - const r = await vscode.window.showQuickPick(['Yes', 'No'], { - canPickMany: false, - placeHolder: 'Do you wish to separate the JSON into multiple files?' - }); - - if (r != null) { - separate = r == 'Yes'; - } else { - return; - } - } else { - separate = setting == 'separate'; - } - } + let reader = new JsonReader(document, name); + let separate = true; - vscode.window.withProgress({ - location: vscode.ProgressLocation.Notification, - cancellable: false - }, async function (progress, token) { - progress.report({ increment: 0, message: 'Generating Data Classes...' }); - scrollTo(0); + if ((await reader.error) == null) { + if (reader.files.length >= 2) { + const setting = readSetting("json.separate"); - await reader.commitJson(progress, separate); + if (setting == "ask") { + const r = await vscode.window.showQuickPick(["Yes", "No"], { + canPickMany: false, + placeHolder: + "Do you wish to separate the JSON into multiple files?", + }); - clearSelection(); - }); + if (r != null) { + separate = r == "Yes"; + } else { + return; + } } else { - showError(await reader.error); + separate = setting == "separate"; + } + } + + vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + cancellable: false, + }, + async function (progress, token) { + progress.report({ + increment: 0, + message: "Generating Data Classes...", + }); + scrollTo(0); + + await reader.commitJson(progress, separate); + + clearSelection(); } - } else if (langId == 'json') { - showError('Please paste the JSON directly into an empty .dart file and then try again!'); + ); } else { - showError('Make sure that you\'re editing a dart file and then try again!'); + showError(await reader.error); } + } else if (langId == "json") { + showError( + "Please paste the JSON directly into an empty .dart file and then try again!" + ); + } else { + showError("Make sure that you're editing a dart file and then try again!"); + } } async function generateDataClass(text = getDocText()) { - if (getLangId() == 'dart') { - const generator = new DataClassGenerator(text); - let clazzes = generator.clazzes; - - if (clazzes.length == 0) { - showError('No convertable dart classes were detected!'); - - return null; - } else if (clazzes.length >= 2) { - // Show a prompt if there is more than one class in the current editor. - clazzes = await showClassChooser(clazzes); - - if (clazzes == null) { - showInfo('No classes selected!'); - return; - } - } + if (getLangId() == "dart") { + const generator = new DataClassGenerator(text); + let clazzes = generator.clazzes; + + if (clazzes.length == 0) { + showError("No convertable dart classes were detected!"); + + return null; + } else if (clazzes.length >= 2) { + // Show a prompt if there is more than one class in the current editor. + clazzes = await showClassChooser(clazzes); + + if (clazzes == null) { + showInfo("No classes selected!"); + return; + } + } + + for (let clazz of clazzes) { + if (clazz.isValid && clazz.toReplace.length > 0) { + if (readSetting("override.manual")) { + // When manual overriding is activated ask for every override. + let result = []; + + for (let replacement of clazz.toReplace) { + const r = await vscode.window.showQuickPick(["Yes", "No"], { + placeHolder: `Do you want to override ${replacement.name}?`, + canPickMany: false, + }); - for (let clazz of clazzes) { - if (clazz.isValid && clazz.toReplace.length > 0) { - if (readSetting('override.manual')) { - // When manual overriding is activated ask for every override. - let result = []; - - for (let replacement of clazz.toReplace) { - const r = await vscode.window.showQuickPick(['Yes', 'No'], { - placeHolder: `Do you want to override ${replacement.name}?`, - canPickMany: false - }); - - if (r == null) { - showInfo('Canceled!'); - return; - } else if ('Yes' == r) result.push(replacement); - } - clazz.toReplace = result; - } - } + if (r == null) { + showInfo("Canceled!"); + return; + } else if ("Yes" == r) result.push(replacement); + } + clazz.toReplace = result; } + } + } - console.log(clazzes); + console.log(clazzes); - const edit = getReplaceEdit(clazzes, generator.imports, true); - await vscode.workspace.applyEdit(edit); + const edit = getReplaceEdit(clazzes, generator.imports, true); + await vscode.workspace.applyEdit(edit); - clearSelection(); + clearSelection(); - return clazzes; - } else { - showError('Make sure that you\'re editing a dart file and then try again!'); - return null; - } + return clazzes; + } else { + showError("Make sure that you're editing a dart file and then try again!"); + return null; + } } /** * @param {DartClass[]} clazzez */ async function showClassChooser(clazzez) { - const values = clazzez.map((v) => v.name); + const values = clazzez.map((v) => v.name); - const r = await vscode.window.showQuickPick(values, { - placeHolder: 'Please select the classes you want to generate data classes of.', - canPickMany: true, - }); + const r = await vscode.window.showQuickPick(values, { + placeHolder: + "Please select the classes you want to generate data classes of.", + canPickMany: true, + }); - let result = []; + let result = []; - if (r != null && r.length > 0) { - for (let c of r) { - for (let clazz of clazzez) { - if (clazz.name == c) - result.push(clazz); - } - } - } else return null; + if (r != null && r.length > 0) { + for (let c of r) { + for (let clazz of clazzez) { + if (clazz.name == c) result.push(clazz); + } + } + } else return null; - return result; + return result; } class DartClass { - constructor() { - /** @type {string} */ - this.name = null; - /** @type {string} */ - this.fullGenericType = ''; - /** @type {string} */ - this.superclass = null; - /** @type {string[]} */ - this.interfaces = []; - /** @type {string[]} */ - this.mixins = []; - /** @type {string} */ - this.constr = null; - /** @type {ClassField[]} */ - this.properties = []; - /** @type {number} */ - this.startsAtLine = null; - /** @type {number} */ - this.endsAtLine = null; - /** @type {number} */ - this.constrStartsAtLine = null; - /** @type {number} */ - this.constrEndsAtLine = null; - this.constrDifferent = false; - this.isArray = false; - this.hasImmutableAnnotation = false; - this.classContent = ''; - this.classType = 'class'; - this.toInsert = ''; - /** @type {ClassPart[]} */ - this.toReplace = []; - this.isLastInFile = false; - } - - get type() { - return this.name + this.genericType; - } - - get genericType() { - const parts = this.fullGenericType.split(','); - - return parts.map((type) => { - let part = type.trim(); - - if (part.includes('extends')) { - part = part.substring(0, part.indexOf('extends')).trim(); - if (type === parts[parts.length - 1]) { - part += '>'; - } - } - - return part; - }).join(', '); + constructor() { + /** @type {string} */ + this.name = null; + /** @type {string} */ + this.fullGenericType = ""; + /** @type {string} */ + this.superclass = null; + /** @type {string[]} */ + this.interfaces = []; + /** @type {string[]} */ + this.mixins = []; + /** @type {string} */ + this.constr = null; + /** @type {ClassField[]} */ + this.properties = []; + /** @type {number} */ + this.startsAtLine = null; + /** @type {number} */ + this.endsAtLine = null; + /** @type {number} */ + this.constrStartsAtLine = null; + /** @type {number} */ + this.constrEndsAtLine = null; + this.constrDifferent = false; + this.isArray = false; + this.hasImmutableAnnotation = false; + this.classContent = ""; + this.classType = "class"; + this.toInsert = ""; + /** @type {ClassPart[]} */ + this.toReplace = []; + this.isLastInFile = false; + } + + get type() { + return this.name + this.genericType; + } + + get genericType() { + const parts = this.fullGenericType.split(","); + + return parts + .map((type) => { + let part = type.trim(); + + if (part.includes("extends")) { + part = part.substring(0, part.indexOf("extends")).trim(); + if (type === parts[parts.length - 1]) { + part += ">"; + } + } + + return part; + }) + .join(", "); + } + + get propsEndAtLine() { + if (this.properties.length > 0) { + return this.properties[this.properties.length - 1].line; + } else { + return -1; } + } - get propsEndAtLine() { - if (this.properties.length > 0) { - return this.properties[this.properties.length - 1].line; - } else { - return -1; - } - } + get hasSuperclass() { + return this.superclass != null; + } - get hasSuperclass() { - return this.superclass != null; - } + get classDetected() { + return this.startsAtLine != null; + } - get classDetected() { - return this.startsAtLine != null; - } + get didChange() { + return ( + this.toInsert.length > 0 || + this.toReplace.length > 0 || + this.constrDifferent + ); + } - get didChange() { - return this.toInsert.length > 0 || this.toReplace.length > 0 || this.constrDifferent; + get hasNamedConstructor() { + if (this.constr != null) { + return this.constr + .replace("const", "") + .trimLeft() + .startsWith(this.name + "({"); } - get hasNamedConstructor() { - if (this.constr != null) { - return this.constr.replace('const', '').trimLeft().startsWith(this.name + '({'); - } + return true; + } - return true; - } + get hasConstructor() { + return ( + this.constrStartsAtLine != null && + this.constrEndsAtLine != null && + this.constr != null + ); + } + + get hasMixins() { + return this.mixins != null && this.mixins.length > 0; + } + + get hasInterfaces() { + return this.interfaces != null && this.interfaces.length > 0; + } + + get hasEnding() { + return this.endsAtLine != null; + } + + get hasProperties() { + return this.properties.length > 0; + } + + get fewProps() { + return this.properties.length <= 3; + } + + get isValid() { + return ( + this.classDetected && + this.hasEnding && + this.hasProperties && + this.uniquePropNames + ); + } - get hasConstructor() { - return this.constrStartsAtLine != null && this.constrEndsAtLine != null && this.constr != null; - } + get isWidget() { + return ( + this.superclass != null && + (this.superclass == "StatelessWidget" || + this.superclass == "StatefulWidget") + ); + } - get hasMixins() { - return this.mixins != null && this.mixins.length > 0; - } + get isStatelessWidget() { + return ( + this.isWidget && + this.superclass != null && + this.superclass == "StatelessWidget" + ); + } - get hasInterfaces() { - return this.interfaces != null && this.interfaces.length > 0; - } + get isState() { + return ( + !this.isWidget && + this.superclass != null && + this.superclass.startsWith("State<") + ); + } - get hasEnding() { - return this.endsAtLine != null; - } + get isAbstract() { + return ( + this.classType == "abstract class" || this.classType == "sealed class" + ); + } - get hasProperties() { - return this.properties.length > 0; - } + get isImmutable() { + return this.properties.every((prop) => prop.isFinal || prop.isConst); + } - get fewProps() { - return this.properties.length <= 3; + get usesEquatable() { + return ( + (this.hasSuperclass && this.superclass == "Equatable") || + (this.hasMixins && this.mixins.includes("EquatableMixin")) + ); + } + + get issue() { + const def = this.name + " couldn't be converted to a data class: "; + let msg = def; + + if (!this.hasProperties) { + msg += "Class must have at least one property!"; + } else if (!this.hasEnding) { + msg += "Class has no ending!"; + } else if (!this.uniquePropNames) { + msg += "Class doesn't have unique property names!"; + } else { + msg = removeEnd(msg, ": ") + "."; } - get isValid() { - return this.classDetected && this.hasEnding && this.hasProperties && this.uniquePropNames; - } + return msg; + } - get isWidget() { - return this.superclass != null && (this.superclass == 'StatelessWidget' || this.superclass == 'StatefulWidget'); - } + get uniquePropNames() { + let props = []; - get isStatelessWidget() { - return this.isWidget && this.superclass != null && this.superclass == 'StatelessWidget'; - } + for (let p of this.properties) { + const n = p.name; - get isState() { - return !this.isWidget && this.superclass != null && this.superclass.startsWith('State<'); - } + if (props.includes(n)) return false; - get isAbstract() { - return this.classType == 'abstract class' || this.classType == 'sealed class'; + props.push(n); } - get isImmutable() { - return this.properties.every((prop) => prop.isFinal || prop.isConst); - } + return true; + } - get usesEquatable() { - return (this.hasSuperclass && this.superclass == 'Equatable') || (this.hasMixins && this.mixins.includes('EquatableMixin')); + /** + * @param {number} line + */ + replacementAtLine(line) { + for (let part of this.toReplace) { + if (part.startsAt <= line && part.endsAt >= line) { + return part.replacement; + } } - get issue() { - const def = this.name + ' couldn\'t be converted to a data class: ' - let msg = def; + return null; + } - if (!this.hasProperties) { - msg += 'Class must have at least one property!'; - } else if (!this.hasEnding) { - msg += 'Class has no ending!'; - } else if (!this.uniquePropNames) { - msg += 'Class doesn\'t have unique property names!'; - } else { - msg = removeEnd(msg, ': ') + '.'; - } - - return msg; - } - - get uniquePropNames() { - let props = []; + generateClassReplacement() { + let replacement = ""; + let lines = this.classContent.split("\n"); - for (let p of this.properties) { - const n = p.name; + for (let i = this.endsAtLine - this.startsAtLine; i >= 0; i--) { + let line = lines[i] + "\n"; + let l = this.startsAtLine + i; - if (props.includes(n)) - return false; + if (i == 0) { + let classDeclaration = ""; - props.push(n); + if ( + readSetting("constructor.immutable") && + !this.hasImmutableAnnotation + ) { + classDeclaration += "@immutable\n"; } - return true; - } + // const classType = this.isAbstract ? 'abstract class' : 'class'; + classDeclaration += + this.classType + " " + this.name + this.fullGenericType; - /** - * @param {number} line - */ - replacementAtLine(line) { - for (let part of this.toReplace) { - if (part.startsAt <= line && part.endsAt >= line) { - return part.replacement; - } + if (this.superclass != null) { + classDeclaration += " extends " + this.superclass; } - return null; - } - - generateClassReplacement() { - let replacement = ''; - let lines = this.classContent.split('\n'); - - for (let i = this.endsAtLine - this.startsAtLine; i >= 0; i--) { - let line = lines[i] + '\n'; - let l = this.startsAtLine + i; - - if (i == 0) { - - let classDeclaration = ''; - - if (readSetting('constructor.immutable') && !this.hasImmutableAnnotation) { - classDeclaration += '@immutable\n'; - } - - // const classType = this.isAbstract ? 'abstract class' : 'class'; - classDeclaration += this.classType + ' ' + this.name + this.fullGenericType; - - if (this.superclass != null) { - classDeclaration += ' extends ' + this.superclass; - } - - /** - * @param {string[]} list - * @param {string} keyword - */ - function addSuperTypes(list, keyword) { - if (list.length == 0) return; - - const length = list.length; - classDeclaration += ` ${keyword} `; - - for (let x = 0; x < length; x++) { - const isLast = x == length - 1; - const type = list[x]; - classDeclaration += type; - - if (!isLast) { - classDeclaration += ', '; - } - } - } - - addSuperTypes(this.mixins, 'with'); - addSuperTypes(this.interfaces, 'implements'); - - classDeclaration += ' {\n'; - replacement = classDeclaration + replacement; - } else if (l == this.propsEndAtLine && this.constr != null && !this.hasConstructor) { - replacement = this.constr + replacement; - replacement = line + replacement; - } else if (l == this.endsAtLine && this.isValid) { - replacement = line + replacement; - replacement = this.toInsert + replacement; - } else { - let rp = this.replacementAtLine(l); - if (rp != null) { - if (!replacement.includes(rp)) - replacement = rp + '\n' + replacement; - } else { - replacement = line + replacement; - } - } + /** + * @param {string[]} list + * @param {string} keyword + */ + function addSuperTypes(list, keyword) { + if (list.length == 0) return; + + const length = list.length; + classDeclaration += ` ${keyword} `; + + for (let x = 0; x < length; x++) { + const isLast = x == length - 1; + const type = list[x]; + classDeclaration += type; + + if (!isLast) { + classDeclaration += ", "; + } + } + } + + addSuperTypes(this.mixins, "with"); + addSuperTypes(this.interfaces, "implements"); + + classDeclaration += " {\n"; + replacement = classDeclaration + replacement; + } else if ( + l == this.propsEndAtLine && + this.constr != null && + !this.hasConstructor + ) { + replacement = this.constr + replacement; + replacement = line + replacement; + } else if (l == this.endsAtLine && this.isValid) { + replacement = line + replacement; + replacement = this.toInsert + replacement; + } else { + let rp = this.replacementAtLine(l); + if (rp != null) { + if (!replacement.includes(rp)) replacement = rp + "\n" + replacement; + } else { + replacement = line + replacement; } - - return removeEnd(replacement, '\n'); + } } + + return removeEnd(replacement, "\n"); + } } class Imports { - /** - * @param {string} text - */ - constructor(text) { - /** @type {string[]} */ - this.values = []; - /** @type {number} */ - this.startAtLine = null; - /** @type {number} */ - this.endAtLine = null; - /** @type {string} */ - this.rawImports = null; - this.text = text; - - this.readImports(); - } - - get hasImports() { - return this.values != null && this.values.length > 0; - } - - get hasExportDeclaration() { - return /^export /m.test(this.formatted); - } - - get hasImportDeclaration() { - return /^import /m.test(this.formatted); - } - - get hasPreviousImports() { - return this.startAtLine != null && this.endAtLine != null; - } - - get didChange() { - return !areStrictEqual(this.rawImports, this.formatted); - } - - get range() { - return new vscode.Range( - new vscode.Position(this.startAtLine - 1, 0), - new vscode.Position(this.endAtLine, 1), + /** + * @param {string} text + */ + constructor(text) { + /** @type {string[]} */ + this.values = []; + /** @type {number} */ + this.startAtLine = null; + /** @type {number} */ + this.endAtLine = null; + /** @type {string} */ + this.rawImports = null; + this.text = text; + + this.readImports(); + } + + get hasImports() { + return this.values != null && this.values.length > 0; + } + + get hasExportDeclaration() { + return /^export /m.test(this.formatted); + } + + get hasImportDeclaration() { + return /^import /m.test(this.formatted); + } + + get hasPreviousImports() { + return this.startAtLine != null && this.endAtLine != null; + } + + get didChange() { + return !areStrictEqual(this.rawImports, this.formatted); + } + + get range() { + return new vscode.Range( + new vscode.Position(this.startAtLine - 1, 0), + new vscode.Position(this.endAtLine, 1) + ); + } + + readImports() { + this.rawImports = ""; + const lines = this.text.split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + const isLast = i == lines.length - 1; + + if ( + line.startsWith("import") || + line.startsWith("export") || + line.startsWith("part") + ) { + this.values.push(line); + this.rawImports += `${line}\n`; + if (this.startAtLine == null) { + this.startAtLine = i + 1; + } + + if (isLast) { + this.endAtLine = i + 1; + break; + } + } else { + const isLicenseComment = + line.startsWith("//") && this.values.length == 0; + const didEnd = !( + isBlank(line) || + line.startsWith("library") || + isLicenseComment ); - } - readImports() { - this.rawImports = ''; - const lines = this.text.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i].trim(); - const isLast = i == lines.length - 1; - - if (line.startsWith('import') || line.startsWith('export') || line.startsWith('part')) { - this.values.push(line); - this.rawImports += `${line}\n`; - if (this.startAtLine == null) { - this.startAtLine = i + 1; - } - - if (isLast) { - this.endAtLine = i + 1; - break; - } + if (isLast || didEnd) { + if (this.startAtLine != null) { + if (i > 0 && isBlank(lines[i - 1])) { + this.endAtLine = i - 1; } else { - const isLicenseComment = line.startsWith('//') && this.values.length == 0; - const didEnd = !(isBlank(line) || line.startsWith('library') || isLicenseComment); - - if (isLast || didEnd) { - if (this.startAtLine != null) { - if (i > 0 && isBlank(lines[i - 1])) { - this.endAtLine = i - 1; - } else { - this.endAtLine = i; - } - } - break; - } + this.endAtLine = i; } + } + break; } + } } + } - get formatted() { - if (!this.hasImports) return ''; + get formatted() { + if (!this.hasImports) return ""; - let workspace = projectName; + let workspace = projectName; - if (workspace == null || workspace.length == 0) { - const file = getEditor().document.uri; - if (file.scheme === 'file') { - const folder = vscode.workspace.getWorkspaceFolder(file); - if (folder) { - workspace = path.basename(folder.uri.fsPath).replace('-', '_'); - } - } - } - - const dartImports = []; - const packageImports = []; - const packageLocalImports = []; - const relativeImports = []; - const partStatements = []; - const exports = []; - - for (let imp of this.values) { - if (imp.startsWith('export')) { - exports.push(imp); - } else if (imp.startsWith('part')) { - partStatements.push(imp); - } else if (imp.includes('dart:')) { - dartImports.push(imp); - } else if (workspace != null && imp.includes(`package:${workspace}`)) { - packageLocalImports.push(imp); - } else if (imp.includes('package:')) { - packageImports.push(imp); - } else { - relativeImports.push(imp); - } + if (workspace == null || workspace.length == 0) { + const file = getEditor().document.uri; + if (file.scheme === "file") { + const folder = vscode.workspace.getWorkspaceFolder(file); + if (folder) { + workspace = path.basename(folder.uri.fsPath).replace("-", "_"); } - - let imps = ''; - - /** - * @param {any[]} imports - */ - function addImports(imports) { - imports.sort(); - for (let i = 0; i < imports.length; i++) { - const isLast = i == imports.length - 1; - const imp = imports[i]; - imps += imp + '\n'; - - if (isLast) { - imps += '\n'; - } - } - } - - function addHeaderLines(text) { - const headerLines = readSetting('custom.headerLines'); - - for (let i = 0; i < headerLines.length; i++) { - const imp = headerLines[i]; - - if (!text.includes(imp)) { - imps += imp + '\n'; - } - } - } - - addHeaderLines(this.text); - addImports(dartImports); - addImports(packageImports); - addImports(packageLocalImports); - addImports(relativeImports); - addImports(exports); - addImports(partStatements); - - return removeEnd(imps, '\n'); + } } - /** - * @param {string} imp - */ - includes(imp) { - return this.values.includes(imp); - } + const dartImports = []; + const packageImports = []; + const packageLocalImports = []; + const relativeImports = []; + const partStatements = []; + const exports = []; - /** - * @param {string} imp - */ - push(imp) { - return this.values.push(imp); + for (let imp of this.values) { + if (imp.startsWith("export")) { + exports.push(imp); + } else if (imp.startsWith("part")) { + partStatements.push(imp); + } else if (imp.includes("dart:")) { + dartImports.push(imp); + } else if (workspace != null && imp.includes(`package:${workspace}`)) { + packageLocalImports.push(imp); + } else if (imp.includes("package:")) { + packageImports.push(imp); + } else { + relativeImports.push(imp); + } } - /** - * @param {string[]} imps - */ - hastAtLeastOneImport(imps) { - for (let imp of imps) { - const impt = `import '${imp}';`; - if (this.text.includes(impt) || this.includes(impt)) - return true; - } - return false; - } + let imps = ""; /** - * @param {string} imp - * @param {string[]} validOverrides + * @param {any[]} imports */ - requiresImport(imp, validOverrides = []) { - const formattedImport = !imp.startsWith('import') ? "import '" + imp + "';" : imp; + function addImports(imports) { + imports.sort(); + for (let i = 0; i < imports.length; i++) { + const isLast = i == imports.length - 1; + const imp = imports[i]; + imps += imp + "\n"; - if (!this.includes(formattedImport) && !this.hastAtLeastOneImport(validOverrides)) { - this.values.push(formattedImport); + if (isLast) { + imps += "\n"; } + } } -} -class ClassField { - /** - * @param {String} type - * @param {String} name - * @param {number} line - * @param {boolean} isFinal - * @param {boolean} isConst - */ - constructor(type, name, line = 1, isFinal = true, isConst = false, json = false) { - this.rawType = type; - this.name = toVarName(name); - this.key = json ? name : varToKey(this.name); - this.line = line; - this.isFinal = isFinal; - this.isConst = isConst; - this.isEnum = false; - this.ignore = false; - this.fromCustom = ['', '', '', '']; // [ 'fromCustom(int ?? 0)' , 'fromCustom' , '(' , 'type ?? def' , ')' ] - this.toCustom = ''; - this.isCollectionType = (/** @type {string} */ type) => this.rawType == type || this.rawType.startsWith(type + '<'); - } + function addHeaderLines(text) { + const headerLines = readSetting("custom.headerLines"); - get type() { - return this.isNullable ? removeEnd(this.rawType, '?') : this.rawType; - } + for (let i = 0; i < headerLines.length; i++) { + const imp = headerLines[i]; - get isCustomFrom() { - this.fromCustom.map(i => (i ?? '').trim()); - return !this.fromCustom.includes(''); + if (!text.includes(imp)) { + imps += imp + "\n"; + } + } } - get isCustomTo() { - return this.toCustom.trim() !== ''; - } + addHeaderLines(this.text); + addImports(dartImports); + addImports(packageImports); + addImports(packageLocalImports); + addImports(relativeImports); + addImports(exports); + addImports(partStatements); - get hasNullCheck() { - return (!this.isPrimitive || this.isCollection) && this.isNullable && !this.ignore; - } + return removeEnd(imps, "\n"); + } - get isNullable() { - return this.rawType.endsWith('?'); - } + /** + * @param {string} imp + */ + includes(imp) { + return this.values.includes(imp); + } - get isList() { - return this.isCollectionType('List'); - } + /** + * @param {string} imp + */ + push(imp) { + return this.values.push(imp); + } - get isMap() { - return this.isCollectionType('Map'); + /** + * @param {string[]} imps + */ + hastAtLeastOneImport(imps) { + for (let imp of imps) { + const impt = `import '${imp}';`; + if (this.text.includes(impt) || this.includes(impt)) return true; } + return false; + } + + /** + * @param {string} imp + * @param {string[]} validOverrides + */ + requiresImport(imp, validOverrides = []) { + const formattedImport = !imp.startsWith("import") + ? "import '" + imp + "';" + : imp; + + if ( + !this.includes(formattedImport) && + !this.hastAtLeastOneImport(validOverrides) + ) { + this.values.push(formattedImport); + } + } +} - get isSet() { - return this.isCollectionType('Set'); - } +class ClassField { + /** + * @param {String} type + * @param {String} name + * @param {number} line + * @param {boolean} isFinal + * @param {boolean} isConst + */ + constructor( + type, + name, + line = 1, + isFinal = true, + isConst = false, + json = false + ) { + this.rawType = type; + this.name = toVarName(name); + this.key = json ? name : varToKey(this.name); + this.line = line; + this.isFinal = isFinal; + this.isConst = isConst; + this.isEnum = false; + this.ignore = false; + this.fromCustom = ["", "", "", ""]; // [ 'fromCustom(int ?? 0)' , 'fromCustom' , '(' , 'type ?? def' , ')' ] + this.toCustom = ""; + this.isCollectionType = (/** @type {string} */ type) => + this.rawType == type || this.rawType.startsWith(type + "<"); + } + + get type() { + return this.isNullable ? removeEnd(this.rawType, "?") : this.rawType; + } + + get isCustomFrom() { + this.fromCustom.map((i) => (i ?? "").trim()); + return !this.fromCustom.includes(""); + } + + get isCustomTo() { + return this.toCustom.trim() !== ""; + } + + get hasNullCheck() { + return ( + (!this.isPrimitive || this.isCollection) && + this.isNullable && + !this.ignore + ); + } - get isCollection() { - return this.isList || this.isMap || this.isSet; - } + get isNullable() { + return this.rawType.endsWith("?"); + } - get subtype() { - if (this.isList || this.isSet) { - const collection = this.isSet ? 'Set' : 'List'; + get isList() { + return this.isCollectionType("List"); + } - const sb = this.rawType.indexOf('<'); // start bracket - const eb = this.rawType.lastIndexOf('>'); // end bracket + get isMap() { + return this.isCollectionType("Map"); + } - const type = this.rawType == collection ? 'dynamic' : this.rawType.substring(sb + 1, eb).trim(); - return new ClassField(type, 'subtype', this.line, this.isFinal); - } - if (this.isMap) { - const sb = this.rawType.lastIndexOf(',') + 1; // start bracket - const eb = this.rawType.lastIndexOf('>'); // end bracket + get isSet() { + return this.isCollectionType("Set"); + } - const valueType = this.rawType.substring(sb, eb).trim(); - return new ClassField(valueType, 'subtype', this.line, this.isFinal); - } + get isCollection() { + return this.isList || this.isMap || this.isSet; + } - return null; - } + get subtype() { + if (this.isList || this.isSet) { + const collection = this.isSet ? "Set" : "List"; - get isSubtype() { - return this.name === 'subtype'; - } + const sb = this.rawType.indexOf("<"); // start bracket + const eb = this.rawType.lastIndexOf(">"); // end bracket - get isPrimitive() { - const t = this.type; - return t == 'String' || t == 'num' || t == 'dynamic' || t == 'bool' || this.isDouble || this.isInt; + const type = + this.rawType == collection + ? "dynamic" + : this.rawType.substring(sb + 1, eb).trim(); + return new ClassField(type, "subtype", this.line, this.isFinal); } + if (this.isMap) { + const sb = this.rawType.lastIndexOf(",") + 1; // start bracket + const eb = this.rawType.lastIndexOf(">"); // end bracket - get base() { - switch (this.type) { - case 'List': return 'Iterable'; - case 'Set': return 'Iterable'; - case 'int': return 'num'; - case 'double': return 'num'; - default: return this.type; - } + const valueType = this.rawType.substring(sb, eb).trim(); + return new ClassField(valueType, "subtype", this.line, this.isFinal); } - get isPrivate() { - return this.name.startsWith('_'); - } + return null; + } - get defValue() { - if (this.isList) { - return 'const []'; - } else if (this.isMap || this.isSet) { - return 'const {}'; - } else { - switch (this.type) { - case 'String': return "''"; - case 'num': - case 'int': return "0"; - case 'double': return "0.0"; - case 'bool': return 'false'; - case 'dynamic': return "null"; - default: return `${this.type}()`; - } - } - } - - get isInt() { - return this.type == 'int'; - } + get isSubtype() { + return this.name === "subtype"; + } - get isDouble() { - return this.type == 'double'; - } + get isPrimitive() { + const t = this.type; + return ( + t == "String" || + t == "num" || + t == "dynamic" || + t == "bool" || + this.isDouble || + this.isInt + ); + } + + get base() { + switch (this.type) { + case "List": + return "Iterable"; + case "Set": + return "Iterable"; + case "int": + return "num"; + case "double": + return "num"; + default: + return this.type; + } + } + + get isPrivate() { + return this.name.startsWith("_"); + } + + get defValue() { + if (this.isList) { + return "const []"; + } else if (this.isMap || this.isSet) { + return "const {}"; + } else { + switch (this.type) { + case "String": + return "''"; + case "num": + case "int": + return "0"; + case "double": + return "0.0"; + case "bool": + return "false"; + case "dynamic": + return "null"; + default: + return `${this.type}()`; + } + } + } + + get isInt() { + return this.type == "int"; + } + + get isDouble() { + return this.type == "double"; + } } class ClassPart { - - /** - * @param {string} name - * @param {number} startsAt - * @param {number} endsAt - * @param {string} current - * @param {string} replacement - */ - constructor(name, startsAt = null, endsAt = null, current = null, replacement = null) { - this.name = name; - this.startsAt = startsAt; - this.endsAt = endsAt; - this.current = current; - this.replacement = replacement; - } - - get isValid() { - return this.startsAt != null && this.endsAt != null && this.current != null; - } - - get startPos() { - return new vscode.Position(this.startsAt, 0); - } - - get endPos() { - return new vscode.Position(this.endsAt, 0); - } + /** + * @param {string} name + * @param {number} startsAt + * @param {number} endsAt + * @param {string} current + * @param {string} replacement + */ + constructor( + name, + startsAt = null, + endsAt = null, + current = null, + replacement = null + ) { + this.name = name; + this.startsAt = startsAt; + this.endsAt = endsAt; + this.current = current; + this.replacement = replacement; + } + + get isValid() { + return this.startsAt != null && this.endsAt != null && this.current != null; + } + + get startPos() { + return new vscode.Position(this.startsAt, 0); + } + + get endPos() { + return new vscode.Position(this.endsAt, 0); + } } class DataClassGenerator { - /** - * @param {String} text - * @param {DartClass[]} clazzes - * @param {boolean} fromJSON - * @param {string} part - */ - constructor(text, clazzes = null, fromJSON = false, part = null) { - this.text = text; - this.fromJSON = fromJSON; - this.clazzes = clazzes == null ? this.parseAndReadClasses() : clazzes; - this.imports = new Imports(text); - this.part = part; - this.generateDataClazzes(); - this.clazz = null; - } - - get hasImports() { - return this.imports.hasImports; - } - - /** - * @param {string} imp - * @param {string[]} validOverrides - */ - requiresImport(imp, validOverrides = []) { - this.imports.requiresImport(imp, validOverrides); - } - - /** - * @param {string} part - */ - isPartSelected(part) { - return this.part == null || this.part == part; - } - - generateDataClazzes() { - const insertConstructor = readSetting('constructor.enabled') && this.isPartSelected('constructor'); - - for (let clazz of this.clazzes) { - this.clazz = clazz; - - if (insertConstructor) - this.insertConstructor(clazz); - - if (!clazz.isWidget) { - if (!clazz.isAbstract) { - if (readSetting('copyWith.enabled') && this.isPartSelected('copyWith')) - this.insertCopyWith(clazz); - if (readSetting('toMap.enabled') && this.isPartSelected('serialization')) - this.insertToMap(clazz); - if (readSetting('fromMap.enabled') && this.isPartSelected('serialization')) - this.insertFromMap(clazz); - if (readSetting('toJson.enabled') && this.isPartSelected('serialization')) - this.insertToJson(clazz); - if (readSetting('fromJson.enabled') && this.isPartSelected('serialization')) - this.insertFromJson(clazz); - } - - if (readSetting('toString.enabled') && this.isPartSelected('toString')) - this.insertToString(clazz); - - if ((clazz.usesEquatable || readSetting('useEquatable')) && this.isPartSelected('useEquatable')) { - this.insertEquatable(clazz); - } else { - if (readSetting('equality.enabled') && this.isPartSelected('equality')) - this.insertEquality(clazz); - if (readSetting('hashCode.enabled') && this.isPartSelected('equality')) - this.insertHash(clazz); - } - } - } - } - - /** - * @param {string} name - * @param {string} finder - * @param {DartClass} clazz - */ - findPart(name, finder, clazz) { - const normalize = (/** @type {string} */ src) => { - let result = ''; - let generics = 0; - let prevChar = ''; - for (const char of src) { - if (char == '<') generics++; - if (char != ' ' && generics == 0) { - result += char; - } - - if (prevChar != '=' && char == '>') generics--; - prevChar = char; - } - - return result; - } - - const finderString = normalize(finder); - const lines = clazz.classContent.split('\n'); - const part = new ClassPart(name); - let curlies = 0; - let singleLine = false; - - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const lineNum = clazz.startsAtLine + i; - - curlies += count(line, '{'); - curlies -= count(line, '}'); - - if (part.startsAt == null && normalize(line).startsWith(finderString)) { - if (line.includes('=>')) singleLine = true; - if (curlies == 2 || singleLine) { - part.startsAt = lineNum; - part.current = line + '\n'; - } - } else if (part.startsAt != null && part.endsAt == null && (curlies >= 2 || singleLine)) { - part.current += line + '\n'; - } else if (part.startsAt != null && part.endsAt == null && curlies == 1) { - part.endsAt = lineNum; - part.current += line; - } - - // Detect the end of a single line function by searching for the ';' because - // a single line function doesn't necessarily only have one single line. - if (singleLine && part.startsAt != null && part.endsAt == null && line.trimRight().endsWith(';')) { - part.endsAt = lineNum; - } - } - - return part.isValid ? part : null; - } - - /** - * If class already exists and has a constructor with the parameter, reuse that parameter. - * E.g. when the dev changed the parameter from this.x to this.x = y the generator inserts - * this.x = y. This way the generator can preserve changes made in the constructor. - * @param {ClassField | string} prop - * @param {{ "name": string; "text": string; "isThis": boolean; }[]} oldProps - */ - findConstrParameter(prop, oldProps) { - const name = typeof prop === 'string' ? prop : prop.name; - for (let oldProp of oldProps) { - if (name === oldProp.name) { - return oldProp; - } - } - - return null; - } - - /** - * @param {DartClass} clazz - */ - findOldConstrProperties(clazz) { - if (!clazz.hasConstructor || clazz.constrStartsAtLine == clazz.constrEndsAtLine) { - return []; - } - - let oldConstr = ''; - let brackets = 0; - let didFindConstr = false; - for (let c of clazz.constr) { - if (c == '(') { - if (didFindConstr) oldConstr += c; - brackets++; - didFindConstr = true; - continue; - } else if (c == ')') { - brackets--; - if (didFindConstr && brackets == 0) - break; - } - - if (brackets >= 1) - oldConstr += c; - } - - oldConstr = removeStart(oldConstr, ['{', '[']); - oldConstr = removeEnd(oldConstr, ['}', ']']); - - let oldArguments = oldConstr.split('\n'); - const oldProperties = []; - for (let arg of oldArguments) { - let formatted = arg.replace('required', '').trim(); - if (formatted.indexOf('=') != -1) { - formatted = formatted.substring(0, formatted.indexOf('=')).trim(); - } - - let name = null; - let isThis = false; - if (formatted.startsWith('this.')) { - name = formatted.replace('this.', ''); - isThis = true; - } else { - const words = formatted.split(' '); - if (words.length >= 1) { - const w = words[1]; - if (!isBlank(w)) name = w; - } - } - - if (name != null) { - oldProperties.push({ - "name": removeEnd(name.trim(), ','), - "text": arg.trim() + '\n', - "isThis": isThis, - }); - } - } - - return oldProperties; - } - - /** - * @param {DartClass} clazz - */ - insertConstructor(clazz) { - const withDefaults = readSetting('constructor.default_values'); - const withImmutable = readSetting('constructor.immutable'); - - let constr = ''; - let startBracket = '({'; - let endBracket = '})'; - - if (withImmutable) { - this.requiresImport(isFlutter ? 'package:flutter/foundation.dart' : 'package:meta/meta.dart'); - } - - if (clazz.constr != null) { - if (clazz.constr.trimLeft().startsWith('const') || withImmutable) { - constr += 'const '; - } - - // Detect custom constructor brackets and preserve them. - const fConstr = clazz.constr.replace('const', '').trimLeft(); - - if (fConstr.startsWith(clazz.name + '([')) startBracket = '(['; - else if (fConstr.startsWith(clazz.name + '({')) startBracket = '({'; - else startBracket = '('; - - if (fConstr.includes('])')) endBracket = '])'; - else if (fConstr.includes('})')) endBracket = '})'; - else endBracket = ')'; + /** + * @param {String} text + * @param {DartClass[]} clazzes + * @param {boolean} fromJSON + * @param {string} part + */ + constructor(text, clazzes = null, fromJSON = false, part = null) { + this.text = text; + this.fromJSON = fromJSON; + this.clazzes = clazzes == null ? this.parseAndReadClasses() : clazzes; + this.imports = new Imports(text); + this.part = part; + this.generateDataClazzes(); + this.clazz = null; + } + + get hasImports() { + return this.imports.hasImports; + } + + /** + * @param {string} imp + * @param {string[]} validOverrides + */ + requiresImport(imp, validOverrides = []) { + this.imports.requiresImport(imp, validOverrides); + } + + /** + * @param {string} part + */ + isPartSelected(part) { + return this.part == null || this.part == part; + } + + generateDataClazzes() { + const insertConstructor = + readSetting("constructor.enabled") && this.isPartSelected("constructor"); + + for (let clazz of this.clazzes) { + this.clazz = clazz; + + if (insertConstructor) this.insertConstructor(clazz); + + if (!clazz.isWidget) { + if (!clazz.isAbstract) { + if ( + readSetting("copyWith.enabled") && + this.isPartSelected("copyWith") + ) + this.insertCopyWith(clazz); + if ( + readSetting("toMap.enabled") && + this.isPartSelected("serialization") + ) + this.insertToMap(clazz); + if ( + readSetting("fromMap.enabled") && + this.isPartSelected("serialization") + ) + this.insertFromMap(clazz); + if ( + readSetting("toJson.enabled") && + this.isPartSelected("serialization") + ) + this.insertToJson(clazz); + if ( + readSetting("fromJson.enabled") && + this.isPartSelected("serialization") + ) + this.insertFromJson(clazz); + } + + if (readSetting("toString.enabled") && this.isPartSelected("toString")) + this.insertToString(clazz); + + if ( + (clazz.usesEquatable || readSetting("useEquatable")) && + this.isPartSelected("useEquatable") + ) { + this.insertEquatable(clazz); } else { - if (clazz.isWidget || clazz.isImmutable || withImmutable) - constr += 'const '; - } - - constr += clazz.name + startBracket + '\n'; - - // Add 'Key key,' for widgets in constructor. - if (clazz.isWidget) { - let hasKey = false; - let clazzConstr = clazz.constr || ''; - for (let line of clazzConstr.split('\n')) { - if (line.trim().startsWith('Key? key')) { - hasKey = true; - break; - } - } - - if (!hasKey) - constr += ' Key? key,\n'; - } - - const oldProperties = this.findOldConstrProperties(clazz); - for (let prop of oldProperties) { - if (!prop.isThis) { - constr += ' ' + prop.text; - } - } - - for (let prop of clazz.properties) { - const oldProperty = this.findConstrParameter(prop, oldProperties); - if (oldProperty != null) { - if (oldProperty.isThis) - constr += ' ' + oldProperty.text; - - continue; - } - - const parameter = `this.${prop.name}` - - constr += ' '; - - if (!prop.isNullable) { - const hasDefault = withDefaults && ((prop.isPrimitive || prop.isCollection) && prop.rawType != 'dynamic'); - const isNamedConstr = startBracket == '({' && endBracket == '})'; - - if (hasDefault) { - constr += `${parameter} = ${prop.defValue},\n`; - } else if (isNamedConstr) { - constr += `required ${parameter},\n`; - } else { - constr += `${parameter},\n`; - } - } else { - constr += `${parameter},\n`; - } - } - - const stdConstrEnd = () => { - constr += endBracket + (clazz.isWidget ? ' : super(key: key);' : ';'); - } - - if (clazz.constr != null) { - let i = null; - if (clazz.constr.includes(' : ')) i = clazz.constr.indexOf(' : ') + 1; - else if (clazz.constr.trimRight().endsWith('{')) i = clazz.constr.lastIndexOf('{'); - - if (i != null) { - let ending = clazz.constr.substring(i, clazz.constr.length); - constr += `${endBracket} ${ending}`; - } else { - stdConstrEnd(); - } - } else { - stdConstrEnd(); - } - - if (clazz.hasConstructor) { - clazz.constrDifferent = !areStrictEqual(clazz.constr, constr); - if (clazz.constrDifferent) { - constr = removeEnd(indent(constr), '\n'); - this.replace(new ClassPart('constructor', clazz.constrStartsAtLine, clazz.constrEndsAtLine, clazz.constr, constr), clazz); - } - } else { - clazz.constrDifferent = true; - this.append(constr, clazz, true); - } - } - - /** - * @param {DartClass} clazz - */ - insertCopyWith(clazz) { - let method = clazz.type + ' copyWith({\n'; - for (const prop of clazz.properties) { - method += ` ${prop.type}? ${prop.name},\n`; - } - method += '}) {\n'; - method += ` return ${clazz.type}(\n`; - - for (let p of clazz.properties) { - method += ` ${clazz.hasNamedConstructor ? `${p.name}: ` : ''}${p.name} ?? this.${p.name},\n`; - } - - method += ' );\n' - method += '}'; - - this.appendOrReplace('copyWith', method, `${clazz.name} copyWith(`, clazz); - } - - /** - * @param {DartClass} clazz - */ - insertToMap(clazz) { - let props = clazz.properties; - /** - * @param {ClassField} prop - */ - function customTypeMapping(prop, name = null, endFlag = ',\n') { - - let nullSafe = ''; - - if (prop.isCollection) { - nullSafe = prop.rawType.match(/<(.+?)>/)[1].endsWith('?') ? '?' : ''; - } else { - nullSafe = prop.isNullable ? '?' : ''; - } - - const typeSetting = readCustomTypeSetting(prop.type); - - if (typeSetting) { - if (typeSetting.toMap === '') { - return `${name ?? prop.name}${endFlag}`; - } - return `${name ?? prop.name}${nullSafe}.${typeSetting.toMap}${endFlag}`; - } - - prop = prop.isCollection ? prop.subtype : prop; - name = name == null ? prop.name : name; - - return `${name}${!prop.isPrimitive ? `${nullSafe}.toMap()` : ''}${endFlag}`; - } - - let method = `Map toMap() {\n`; - method += ' return {\n'; - for (let p of props) { - method += ` '${p.key}': `; - const nullSafe = p.isNullable ? '?' : ''; - - if (p.ignore) { - method += `${p.name},\n`; - } else - if (p.isCustomTo) { - method += `${p.name}${nullSafe}.${p.toCustom},\n`; - } else - if (p.isEnum) { - const setting = readSetting('json.enum_format'); - const toEnum = setting === 'byIndex' ? 'index' : 'name'; - - method += `${p.name}${nullSafe}.${toEnum},\n`; - } else if (p.isCollection) { - const nullSafeSub = p.type.match(/<(.+?)>/)[1].endsWith('?') ? '?' : ''; - - if (p.isMap || p.subtype.isPrimitive) { - const mapFlag = p.isSet ? `${nullSafe}.toList()` : ''; - method += `${p.name}${mapFlag},\n`; - } else { - - method += `${p.name}${nullSafe}.map((x) => ${customTypeMapping(p.subtype, 'x', '')})${nullSafeSub}.toList(),\n` - } - } else { - method += customTypeMapping(p); - } - if (p.name == props[props.length - 1].name) method += ' };\n'; - } - method += '}'; - - this.appendOrReplace('toMap', method, 'Map toMap()', clazz); - } - - /** - * @param {DartClass} clazz - */ - insertFromMap(clazz) { - let withDefaultValues = readSetting('fromMap.default_values'); - let withStrictNumbers = readSetting('strict_numbers'); - let props = clazz.properties; - - - /** - * @param {ClassField} p - */ - function retype(p) { - let type = p.type; - let suffix = ''; - - if (!withStrictNumbers && (p.isInt || p.isDouble)) { - type = 'num'; - suffix = p.isDouble ? '.toDouble()' : '.toInt()'; - } else if (p.isList || p.isSet) { - type = 'Iterable'; - - if (p.subtype.isMap) { - suffix = `.map((x) => ${p.subtype.rawType}.from(x as Map))`; - } else if (p.subtype.isCollection) { - suffix = `.map((x) => ${p.subtype.rawType}.from(x as Iterable))`; - } - - } else if (p.isMap) { - type = 'Map'; - } - - let nullable = (p.isNullable || withDefaultValues) && !p.hasNullCheck ? '?' : ''; - type += nullable; - - // Adjust suffix for nullable types - suffix = nullable && suffix ? '?' + suffix : suffix; - - return [type, suffix]; - } - - /** - * @param {ClassField} p - * @param {string} customType - */ - function cast(p, customType = null) { - const [type, suffix] = retype(p); - return `cast<${customType ?? type}>('${p.key}')${suffix}`; - } - - - /** - * @param {ClassField} p - */ - function customTypeMapping(p) { - - function defVal(value) { - if (!value) return ''; - return withDefaultValues && !p.isNullable ? ` ?? ${value}` : ''; - } - - const typeSetting = readCustomTypeSetting(p.type); - - if (typeSetting) { - - if (typeSetting.fromMap === '') { - return cast(p); - } - - const [from, open1, typedef, close1] = extractFromMap(typeSetting.fromMap); - const [stype, def] = typedef.split('??').map(i => (i ?? '').trim()); - - const hasDef = (def ?? '') !== '' && !p.hasNullCheck; - const defValue = hasDef ? ` ?? ${def}` : ''; - - const type = stype === '' ? p.type : stype; - const dot = from === '' ? '' : `.${from}`; - - const withDefaultValues = readSetting('fromMap.default_values'); - - if (p.isSubtype) { - - const inclass = new ClassField(stype, 'type', p.line); - let [intype, suffix] = retype(inclass); - - // we add brackets if we have a suffix - const [open2, close2] = suffix === '' ? ['', ''] : ['(', ')']; - - if (withDefaultValues) { - suffix += ` ?? ${inclass.defValue}`; - } - - return `${p.type}${dot}${open1}${open2}x as ${intype}${hasDef ? '? ??' : ''}${defValue}${close2}${suffix}${close1}`; - } - - return `${p.type}${dot}${open1}cast<${type}${hasDef ? '?' : ''}>('${p.key}')${defValue}${close1}`; - } - - if (p.isSubtype) { - return `${p.type}.fromMap(Map.from(x as Map))`; - } - return `${p.type}.fromMap(Map.from(${cast(p, 'Map')}${defVal('{}')}))`; - } - - const customError = readSetting('custom.argumentError'); - - let method = `factory ${clazz.name}.fromMap(Map map) {\n`; - method += ` T cast(String k) => map[k] is T ? map[k] as T : ${customError}\n`; - method += ' return ' + clazz.type + '(\n'; - for (let p of props) { - method += ` ${clazz.hasNamedConstructor ? `${p.name}: ` : ''}`; - - // const value = `cast<${p.base}>('${p}')`; - const value = cast(p); - const hasNullCheck = p.hasNullCheck; - - if (hasNullCheck) { - method += `map['${p.key}'] != null ? `; - } - - function defVal(value) { - return withDefaultValues && !p.isNullable ? ` ?? ${value}` : ''; - } - - // custom serialization - if (p.ignore) { - method += `cast<${p.rawType}>('${p.key}')`; - } else if (p.isCustomFrom) { - const [from, open, typedef, close] = p.fromCustom; - const [type, def] = typedef.split('??').map(i => (i ?? '').trim()); - - const hasDef = (def ?? '') !== '' && !p.hasNullCheck; - const putDef = hasDef ? ` ?? ${def}` : ''; - - method += `${p.type}.${from}${open}cast<${type}${hasDef ? '?' : ''}>('${p.key}')${putDef}${close}`; - - // serialization - } else if (p.isEnum) { - const setting = readSetting('json.enum_format'); - const evalues = p.type + '.values'; - - if (setting === 'byIndex') { - p.rawType = 'int'; - method += `${evalues}[${cast(p)}${defVal('0')}]`; - } else { - p.rawType = 'String'; - method += `${evalues}.byName(${cast(p)}${defVal(`${evalues}.first.name`)})`; - } - - } else if (p.isCollection) { - let listSubtype = p.type.match(/<(.+?)>/)[1]; - if (listSubtype.startsWith('Map')) listSubtype = listSubtype + '>'; - - const defaultValue = withDefaultValues && !p.isNullable ? ` ?? const ${p.isMap ? '{}' : p.isList ? `<${listSubtype}>[]` : `<${listSubtype}>{}` - }` : ''; - - method += `${p.type}.from(`; - if (p.subtype.isPrimitive || p.subtype.isCollection) { - method += `${value}${defaultValue})`; - } else { - const qm = defaultValue === '' ? '' : '?'; - method += `cast('${p.name}')${qm}.map((x) => ${customTypeMapping(p.subtype)})${defaultValue})`; - } - } else if (p.isPrimitive) { - const defaultValue = withDefaultValues && !p.isNullable ? ` ?? ${p.defValue} ` : ''; - method += `${value}${defaultValue}`; - } else { - method += customTypeMapping(p); - } - - if (hasNullCheck) { - method += ` : null`; - } - - method += ',\n'; - - const isLast = p.name == props[props.length - 1].name; - if (isLast) method += ' );\n'; - } - method += '}'; - - this.appendOrReplace('fromMap', method, `factory ${clazz.name}.fromMap(Map map)`, clazz); - } - - /** - * @param {DartClass} clazz - */ - insertToJson(clazz) { - this.requiresImport('dart:convert'); - - const method = 'String toJson() => json.encode(toMap());'; - this.appendOrReplace('toJson', method, 'String toJson()', clazz); - } - - /** - * @param {DartClass} clazz - */ - insertFromJson(clazz) { - this.requiresImport('dart:convert'); - - const method = `factory ${clazz.name}.fromJson(String source) => ${clazz.name}.fromMap(json.decode(source) as Map);`; - this.appendOrReplace('fromJson', method, `factory ${clazz.name}.fromJson(String source)`, clazz); - } - - /** - * @param {DartClass} clazz - */ - insertToString(clazz) { - const short = clazz.fewProps; - const props = clazz.properties; - let method = '@override\n'; - method += `String toString() ${!short ? '{\n' : '=>\n'}`; - method += `${!short ? ' return' : ''} '` + `${clazz.name}(`; - for (let p of props) { - const name = p.name; - const isFirst = name == props[0].name; - const isLast = name == props[props.length - 1].name; - - if (!isFirst) - method += ' '; - - method += name + ': $' + name + ','; - - if (isLast) { - method = removeEnd(method, ','); - method += ")';" + (short ? '' : '\n'); - } - } - method += !short ? '}' : ''; - - this.appendOrReplace('toString', method, 'String toString()', clazz); - } - - /** - * @param {DartClass} clazz - */ - insertEquality(clazz) { - const props = clazz.properties; - const hasCollection = props.find((p) => p.isCollection) != undefined; - - let collectionEqualityFn; - if (hasCollection) { - // Flutter already has collection equality functions - // in the foundation package. - if (isFlutter) { - this.requiresImport('package:flutter/foundation.dart'); - } else { - this.requiresImport('package:collection/collection.dart'); - - collectionEqualityFn = 'collectionEquals'; - const isListOnly = props.find((p) => p.isCollection && !p.isList) == undefined; - if (isListOnly) collectionEqualityFn = 'listEquals'; - const isMapOnly = props.find((p) => p.isCollection && !p.isMap) == undefined; - if (isMapOnly) collectionEqualityFn = 'mapEquals'; - const isSetOnly = props.find((p) => p.isCollection && !p.isSet) == undefined; - if (isSetOnly) collectionEqualityFn = 'setEquals'; - } - } - - let method = '@override\n'; - method += 'bool operator ==(Object other) {\n'; - method += ' if (identical(this, other)) return true;\n'; - if (hasCollection && !isFlutter) - method += ` final ${collectionEqualityFn} = const DeepCollectionEquality().equals;\n` - method += '\n'; - method += ' return other is ' + clazz.type + ' &&\n'; - for (let prop of props) { - if (prop.isCollection) { - if (isFlutter) collectionEqualityFn = prop.isSet ? 'setEquals' : prop.isMap ? 'mapEquals' : 'listEquals'; - method += ` ${collectionEqualityFn}(other.${prop.name}, ${prop.name})`; - } else { - method += ` other.${prop.name} == ${prop.name}`; - } - if (prop.name != props[props.length - 1].name) method += ' &&\n'; - else method += ';\n'; - } - method += '}'; - - this.appendOrReplace('equality', method, 'bool operator ==', clazz); - } - - /** - * @param {DartClass} clazz - */ - insertHash(clazz) { - const useJenkins = readSetting('hashCode.use_jenkins'); - const short = !useJenkins && clazz.fewProps; - const props = clazz.properties; - let method = '@override\n'; - method += `int get hashCode ${short ? '=>' : '{\n return '}`; - - if (useJenkins) { - // dart:ui import is required for Jenkins hash. - this.requiresImport('dart:ui', [ - 'package:flutter/material.dart', - 'package:flutter/cupertino.dart', - 'package:flutter/widgets.dart' - ]); - - method += `hashList([\n`; - for (let p of props) { - method += ' ' + p.name + `,\n`; - } - method += ' ]);'; - } else { - for (let p of props) { - const isFirst = p == props[0]; - method += `${isFirst && !short ? '' : short ? ' ' : ' '}${p.name}.hashCode`; - if (p == props[props.length - 1]) { - method += ';'; - } else { - method += ` ^${!short ? '\n' : ''}`; - } - } - } - - if (!short) method += '\n}'; - - this.appendOrReplace('hashCode', method, 'int get hashCode', clazz); - } - - /** - * @param {DartClass} clazz - */ - addEquatableDetails(clazz) { - // Do not generate Equatable for class with 'Base' in their - // names as Base classes should inherit from Equatable. - // see: https://github.com/arthurbcd/dart-safe-data-class-generator/issues/8 - if (clazz.hasSuperclass && clazz.superclass.includes('Base')) return; - - this.requiresImport('package:equatable/equatable.dart'); + if ( + readSetting("equality.enabled") && + this.isPartSelected("equality") + ) + this.insertEquality(clazz); + if ( + readSetting("hashCode.enabled") && + this.isPartSelected("equality") + ) + this.insertHash(clazz); + } + } + } + } + + /** + * @param {string} name + * @param {string} finder + * @param {DartClass} clazz + */ + findPart(name, finder, clazz) { + const normalize = (/** @type {string} */ src) => { + let result = ""; + let generics = 0; + let prevChar = ""; + for (const char of src) { + if (char == "<") generics++; + if (char != " " && generics == 0) { + result += char; + } + + if (prevChar != "=" && char == ">") generics--; + prevChar = char; + } + + return result; + }; - if (!clazz.usesEquatable) { - if (clazz.hasSuperclass) { - this.addMixin('EquatableMixin'); - } else { - this.setSuperClass('Equatable'); - } - } + const finderString = normalize(finder); + const lines = clazz.classContent.split("\n"); + const part = new ClassPart(name); + let curlies = 0; + let singleLine = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineNum = clazz.startsAtLine + i; + + curlies += count(line, "{"); + curlies -= count(line, "}"); + + if (part.startsAt == null && normalize(line).startsWith(finderString)) { + if (line.includes("=>")) singleLine = true; + if (curlies == 2 || singleLine) { + part.startsAt = lineNum; + part.current = line + "\n"; + } + } else if ( + part.startsAt != null && + part.endsAt == null && + (curlies >= 2 || singleLine) + ) { + part.current += line + "\n"; + } else if (part.startsAt != null && part.endsAt == null && curlies == 1) { + part.endsAt = lineNum; + part.current += line; + } + + // Detect the end of a single line function by searching for the ';' because + // a single line function doesn't necessarily only have one single line. + if ( + singleLine && + part.startsAt != null && + part.endsAt == null && + line.trimRight().endsWith(";") + ) { + part.endsAt = lineNum; + } + } + + return part.isValid ? part : null; + } + + /** + * If class already exists and has a constructor with the parameter, reuse that parameter. + * E.g. when the dev changed the parameter from this.x to this.x = y the generator inserts + * this.x = y. This way the generator can preserve changes made in the constructor. + * @param {ClassField | string} prop + * @param {{ "name": string; "text": string; "isThis": boolean; }[]} oldProps + */ + findConstrParameter(prop, oldProps) { + const name = typeof prop === "string" ? prop : prop.name; + for (let oldProp of oldProps) { + if (name === oldProp.name) { + return oldProp; + } + } + + return null; + } + + /** + * @param {DartClass} clazz + */ + findOldConstrProperties(clazz) { + if ( + !clazz.hasConstructor || + clazz.constrStartsAtLine == clazz.constrEndsAtLine + ) { + return []; + } + + let oldConstr = ""; + let brackets = 0; + let didFindConstr = false; + for (let c of clazz.constr) { + if (c == "(") { + if (didFindConstr) oldConstr += c; + brackets++; + didFindConstr = true; + continue; + } else if (c == ")") { + brackets--; + if (didFindConstr && brackets == 0) break; + } + + if (brackets >= 1) oldConstr += c; + } + + oldConstr = removeStart(oldConstr, ["{", "["]); + oldConstr = removeEnd(oldConstr, ["}", "]"]); + + let oldArguments = oldConstr.split("\n"); + const oldProperties = []; + for (let arg of oldArguments) { + let formatted = arg.replace("required", "").trim(); + if (formatted.indexOf("=") != -1) { + formatted = formatted.substring(0, formatted.indexOf("=")).trim(); + } + + let name = null; + let isThis = false; + if (formatted.startsWith("this.")) { + name = formatted.replace("this.", ""); + isThis = true; + } else { + const words = formatted.split(" "); + if (words.length >= 1) { + const w = words[1]; + if (!isBlank(w)) name = w; + } + } + + if (name != null) { + oldProperties.push({ + name: removeEnd(name.trim(), ","), + text: arg.trim() + "\n", + isThis: isThis, + }); + } } - /** - * @param {DartClass} clazz - */ - insertEquatable(clazz) { - this.addEquatableDetails(clazz); - - const props = clazz.properties; - const short = props.length <= 4; - const split = short ? ', ' : ',\n'; - let method = '@override\n'; - method += `List get props ${!short ? '{\n' : '=>'}`; - method += `${!short ? ' return' : ''} ` + '[' + (!short ? '\n' : ''); - for (let prop of props) { - const isLast = prop.name == props[props.length - 1].name; - const inset = !short ? ' ' : ''; - method += inset + prop.name + split; - - if (isLast) { - if (short) method = removeEnd(method, split); - method += (!short ? ' ' : '') + '];' + (!short ? '\n' : ''); - } - } - method += !short ? '}' : ''; - - this.appendOrReplace('props', method, 'List get props', clazz); - } + return oldProperties; + } - /** - * @param {string} mixin - */ - addMixin(mixin) { - const mixins = this.clazz.mixins; - if (!mixins.includes(mixin)) { - mixins.push(mixin); - } - } + /** + * @param {DartClass} clazz + */ + insertConstructor(clazz) { + const withDefaults = readSetting("constructor.default_values"); + const withImmutable = readSetting("constructor.immutable"); - /** - * @param {string} impl - */ - addInterface(impl) { - const interfaces = this.clazz.interfaces; - if (!interfaces.includes(impl)) { - interfaces.push(impl); - } + let constr = ""; + let startBracket = "({"; + let endBracket = "})"; + + if (withImmutable) { + this.requiresImport( + isFlutter ? "package:flutter/foundation.dart" : "package:meta/meta.dart" + ); } - /** - * @param {string} clazz - */ - setSuperClass(clazz) { - this.clazz.superclass = clazz; + if (clazz.constr != null) { + if (clazz.constr.trimLeft().startsWith("const") || withImmutable) { + constr += "const "; + } + + // Detect custom constructor brackets and preserve them. + const fConstr = clazz.constr.replace("const", "").trimLeft(); + + if (fConstr.startsWith(clazz.name + "([")) startBracket = "(["; + else if (fConstr.startsWith(clazz.name + "({")) startBracket = "({"; + else startBracket = "("; + + if (fConstr.includes("])")) endBracket = "])"; + else if (fConstr.includes("})")) endBracket = "})"; + else endBracket = ")"; + } else { + if (clazz.isWidget || clazz.isImmutable || withImmutable) + constr += "const "; } - /** - * @param {string} name - * @param {string} n - * @param {string} finder - * @param {DartClass} clazz - */ - appendOrReplace(name, n, finder, clazz) { - let part = this.findPart(name, finder, clazz); - let replacement = removeEnd(indent(n.replace('@override\n', '')), '\n'); - - if (part != null) { - part.replacement = replacement; - if (!areStrictEqual(part.current, part.replacement)) { - this.replace(part, clazz); - } - } else { - this.append(n, clazz); + constr += clazz.name + startBracket + "\n"; + + // Add 'Key key,' for widgets in constructor. + if (clazz.isWidget) { + let hasKey = false; + let clazzConstr = clazz.constr || ""; + for (let line of clazzConstr.split("\n")) { + if (line.trim().startsWith("Key? key")) { + hasKey = true; + break; } - } + } - /** - * @param {string} method - * @param {DartClass} clazz - */ - append(method, clazz, constr = false) { - let met = indent(method); - constr ? clazz.constr = met : clazz.toInsert += '\n' + met; + if (!hasKey) constr += " Key? key,\n"; } - /** - * @param {ClassPart} part - * @param {DartClass} clazz - */ - replace(part, clazz) { - clazz.toReplace.push(part); + const oldProperties = this.findOldConstrProperties(clazz); + for (let prop of oldProperties) { + if (!prop.isThis) { + constr += " " + prop.text; + } } - parseAndReadClasses() { - let clazzes = []; - let clazz = new DartClass(); + for (let prop of clazz.properties) { + const oldProperty = this.findConstrParameter(prop, oldProperties); + if (oldProperty != null) { + if (oldProperty.isThis) constr += " " + oldProperty.text; - let lines = this.text.split('\n'); - let curlyBrackets = 0; - let brackets = 0; + continue; + } - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const linePos = i + 1; - // Make sure to look for 'class ' with the space in order to allow - // fields that contain the word 'class' as in classifire. - // issue: https://github.com/arthurbcd/dart-data-class-tools/issues/2 + const parameter = `this.${prop.name}`; - const prefixes = ['class ', 'abstract class ', 'sealed class ', 'final class ']; + constr += " "; - function testPrefix(prefix) { - const found = line.trim().startsWith(prefix); - if (found) { - clazz.classType = prefix.trim(); - return true; - } - return false; - } + if (!prop.isNullable) { + const hasDefault = + withDefaults && + (prop.isPrimitive || prop.isCollection) && + prop.rawType != "dynamic"; + const isNamedConstr = startBracket == "({" && endBracket == "})"; - const classLine = prefixes.some(testPrefix); + if (hasDefault) { + constr += `${parameter} = ${prop.defValue},\n`; + } else if (isNamedConstr) { + constr += `required ${parameter},\n`; + } else { + constr += `${parameter},\n`; + } + } else { + constr += `${parameter},\n`; + } + } - // const classLine = line.trimLeft().startsWith('class ') || line.trimLeft().startsWith('abstract class ') || line.trimLeft().startsWith('sealed class '); + const stdConstrEnd = () => { + constr += endBracket + (clazz.isWidget ? " : super(key: key);" : ";"); + }; - if (classLine) { - // clazz = new DartClass(); - clazz.startsAtLine = linePos; + if (clazz.constr != null) { + let i = null; + if (clazz.constr.includes(" : ")) i = clazz.constr.indexOf(" : ") + 1; + else if (clazz.constr.trimRight().endsWith("{")) + i = clazz.constr.lastIndexOf("{"); + + if (i != null) { + let ending = clazz.constr.substring(i, clazz.constr.length); + constr += `${endBracket} ${ending}`; + } else { + stdConstrEnd(); + } + } else { + stdConstrEnd(); + } + + if (clazz.hasConstructor) { + clazz.constrDifferent = !areStrictEqual(clazz.constr, constr); + if (clazz.constrDifferent) { + constr = removeEnd(indent(constr), "\n"); + this.replace( + new ClassPart( + "constructor", + clazz.constrStartsAtLine, + clazz.constrEndsAtLine, + clazz.constr, + constr + ), + clazz + ); + } + } else { + clazz.constrDifferent = true; + this.append(constr, clazz, true); + } + } - if (lines[linePos - 2].includes('@immutable')) { - clazz.hasImmutableAnnotation = true; - } + /** + * @param {DartClass} clazz + */ + insertCopyWith(clazz) { + let method = clazz.type + " copyWith({\n"; + for (const prop of clazz.properties) { + method += ` ${prop.type}? ${prop.name},\n`; + } + method += "}) {\n"; + method += ` return ${clazz.type}(\n`; - let classNext = false; - let extendsNext = false; - let implementsNext = false; - let mixinsNext = false; - - // Reset brackets count when a new class was detected. - curlyBrackets = 0; - brackets = 0; - - const words = this.splitWhileMaintaingGenerics(line); - for (let word of words) { - word = word.trim(); - if (word.length > 0) { - if (word == 'class') { - classNext = true; - } else if (word == 'extends') { - extendsNext = true; - } else if (extendsNext) { - extendsNext = false; - clazz.superclass = word; - } else if (word == 'with') { - mixinsNext = true; - extendsNext = false; - implementsNext = false; - } else if (word == 'implements') { - mixinsNext = false; - extendsNext = false; - implementsNext = true; - } else if (classNext) { - classNext = false; - - // Remove generics from class name. - if (word.includes('<')) { - clazz.fullGenericType = word.substring( - word.indexOf('<'), - word.lastIndexOf('>') + 1, - ); - - word = word.substring(0, word.indexOf('<')); - } - - clazz.name = word; - } else if (mixinsNext) { - const mixin = removeEnd(word, ',').trim(); - - if (mixin.length > 0) { - clazz.mixins.push(mixin); - } - } else if (implementsNext) { - const impl = removeEnd(word, ',').trim(); - - - if (impl.length > 0) { - clazz.interfaces.push(impl); - } - } - } - } + for (let p of clazz.properties) { + method += ` ${clazz.hasNamedConstructor ? `${p.name}: ` : ""}${ + p.name + } ?? this.${p.name},\n`; + } - // Do not add State classes of widgets. - if (!clazz.isState) { - clazzes.push(clazz); - } - } + method += " );\n"; + method += "}"; - if (clazz.classDetected) { - // Check if class ended based on bracket count. If all '{' have a '}' pair, - // class can be closed. - curlyBrackets += count(line, '{'); - curlyBrackets -= count(line, '}'); - // Count brackets, e.g. to find the constructor. - brackets += count(line, '('); - brackets -= count(line, ')'); - - // Detect beginning of constructor by looking for the class name and a bracket, while also - // making sure not to falsely detect a function constructor invocation with the actual - // constructor with boilerplaty checking all possible constructor options. - const includesConstr = line.replace('const', '').trimLeft().startsWith(clazz.name + '('); - if (includesConstr && !classLine) { - clazz.constrStartsAtLine = linePos; - } + this.appendOrReplace("copyWith", method, `${clazz.name} copyWith(`, clazz); + } - if (clazz.constrStartsAtLine != null && clazz.constrEndsAtLine == null) { - clazz.constr = clazz.constr == null ? line + '\n' : clazz.constr + line + '\n'; + /** + * @param {DartClass} clazz + */ + insertToMap(clazz) { + let props = clazz.properties; + /** + * @param {ClassField} prop + */ + function customTypeMapping(prop, name = null, endFlag = ",\n") { + let nullSafe = ""; - // Detect end of constructor. - if (brackets == 0) { - clazz.constrEndsAtLine = linePos; - clazz.constr = removeEnd(clazz.constr, '\n'); - } - } + if (prop.isCollection) { + nullSafe = prop.rawType.match(/<(.+?)>/)[1].endsWith("?") ? "?" : ""; + } else { + nullSafe = prop.isNullable ? "?" : ""; + } - clazz.classContent += line; - // Detect end of class. - if (curlyBrackets != 0) { - clazz.classContent += '\n'; - } else { - clazz.endsAtLine = linePos; - clazz = new DartClass(); - } + const typeSetting = readCustomTypeSetting(prop.type); - if (brackets == 0 && curlyBrackets == 1) { - // Check if a line is valid to only include real properties. - const lineValid = - // Line shouldn't start with the class name as this would - // be the constructor or an error. - !line.trimLeft().startsWith(clazz.name) && - // Ignore comments. - !line.trimLeft().startsWith('//') && - // These symbols would indicate that this is not a field. - !includesOne(line, ['{', '}', '=>', '@'], false) && - // Filter out some keywords. - !includesOne(line, ['static', 'set', 'get', 'return', 'factory']) && - // Do not include final values that are assigned a value. - !includesAll(line, ['final ', '=']) && - // Do not inlcude non final fields that were declared after the constructor. - (clazz.constrStartsAtLine == null || line.includes('final ')) && - // Make sure not to catch abstract functions. - !line.replace(/\s/g, '').endsWith(');'); - - if (lineValid) { - let type = null; - let name = null; - let isFinal = false; - let isConst = false; - - const words = line.trim().split(' '); - for (let i = 0; i < words.length; i++) { - const word = words[i]; - const isLast = i == words.length - 1; - - if (word.length > 0 && word != '}' && word != '{') { - if (word == 'final') { - isFinal = true; - } else if (i == 0 && word == 'const') { - isConst = true; - } - - // Be sure to not include keywords. - if (word != 'final' && word != 'const') { - // If word ends with semicolon => variable name, else type. - let isVariable = word.endsWith(';') || (!isLast && (words[i + 1] == '=')); - // Make sure we don't capture abstract functions like: String func(); - isVariable = isVariable && !includesOne(word, ['(', ')']); - if (isVariable) { - if (name == null) - name = removeEnd(word, ';'); - } else { - if (type == null) type = word; - // Types can have gaps => Pair, - // thus append word to type if a name hasn't - // been detected. - else if (name == null) type += ' ' + word; - } - } - } - } - - if (type != null && name != null) { - const prop = new ClassField(type, name, linePos, isFinal, isConst); - - if (i > 0) { - const prevLine = lines[i - 1]; - - // Ignore comments, search for directives. - const commentParts = lines[i].split("//"); - const directives = commentParts.length > 1 ? commentParts[1].trim() : ''; - - // Check if is Enum. - - prop.ignore = !!(directives === 'ignore'); - prop.isEnum = !!(prevLine.match(/^\s*\/\/(\s*)enum/) || directives === 'enum'); - - // Custom serialization. - const [from, to] = directives.split(",").map(i => i.trim()); - - if (from !== '') { - // // fromCustom(type ?? def) || fromCustom[type ?? def] - // const regex = /(\w+)([\[(])((?:[^()\[\]]|\((?:[^()]*\)))*)((?:[\])]))/; - - // extractFromMap(fromMap) - - // const r = regex.exec(from); - prop.fromCustom = extractFromMap(from); - } - if (to !== '') prop.toCustom = to ?? ''; - } - - clazz.properties.push(prop); - } - } - } - } + if (typeSetting) { + if (typeSetting.toMap === "") { + return `${name ?? prop.name}${endFlag}`; } + return `${name ?? prop.name}${nullSafe}.${typeSetting.toMap}${endFlag}`; + } + + prop = prop.isCollection ? prop.subtype : prop; + name = name == null ? prop.name : name; - return clazzes; + return `${name}${ + !prop.isPrimitive ? `${nullSafe}.toMap()` : "" + }${endFlag}`; } - /** - * This function is for parsing the class name line while maintaining - * also more complex generic types like class A>. - * - * @param {string} line - */ - splitWhileMaintaingGenerics(line) { - let words = []; - let index = 0; - let generics = 0; - for (let i = 0; i < line.length; i++) { - const char = line[i]; - const isCurly = char == '{'; - const isSpace = char == ' '; - - if (char == '<') generics++; - if (char == '>') generics--; - - if (generics == 0 && (isSpace || isCurly)) { - const word = line.substring(index, i).trim(); - - // Do not add whitespace. - if (word.length == 0) continue; - const isOnlyGeneric = word.startsWith('<'); - - // Append the generic type to the word when there is spacing - // between them. E.g.: class Hello - if (isOnlyGeneric) { - words[words.length - 1] = words[words.length - 1] + word; - } else { - words.push(word); - } + let method = `Map toMap() {\n`; + method += " return {\n"; + for (let p of props) { + method += ` '${p.key}': `; + const nullSafe = p.isNullable ? "?" : ""; - if (isCurly) { - break; - } + if (p.ignore) { + method += `${p.name},\n`; + } else if (p.isCustomTo) { + method += `${p.name}${nullSafe}.${p.toCustom},\n`; + } else if (p.isEnum) { + const setting = readSetting("json.enum_format"); + const toEnum = setting === "byIndex" ? "index" : "name"; - index = i; - } - } + method += `${p.name}${nullSafe}.${toEnum},\n`; + } else if (p.isCollection) { + const nullSafeSub = p.type.match(/<(.+?)>/)[1].endsWith("?") ? "?" : ""; - return words; - } -} + if (p.isMap || p.subtype.isPrimitive) { + const mapFlag = p.isSet ? `${nullSafe}.toList()` : ""; + method += `${p.name}${mapFlag},\n`; + } else { + method += `${p.name}${nullSafe}.map((x) => ${customTypeMapping( + p.subtype, + "x", + "" + )})${nullSafeSub}.toList(),\n`; + } + } else { + method += customTypeMapping(p); + } + if (p.name == props[props.length - 1].name) method += " };\n"; + } + method += "}"; + + this.appendOrReplace( + "toMap", + method, + "Map toMap()", + clazz + ); + } -class DartFile { - /** - * @param {DartClass} clazz - * @param {string} content - */ - constructor(clazz, content = null) { - this.clazz = clazz; - this.name = createFileName(clazz.name); - this.content = content || clazz.classContent; - } -} + /** + * @param {DartClass} clazz + */ + insertFromMap(clazz) { + let withDefaultValues = readSetting("fromMap.default_values"); + let withStrictNumbers = readSetting("strict_numbers"); + let props = clazz.properties; -class JsonReader { /** - * @param {string} source - * @param {string} className + * @param {ClassField} p */ - constructor(source, className) { - this.json = this.toPlainJson(source); + function retype(p) { + let type = p.type; + let suffix = ""; - this.clazzName = capitalize(className); - /** @type {DartClass[]} */ - this.clazzes = []; - /** @type {DartFile[]} */ - this.files = []; + if (!withStrictNumbers && (p.isInt || p.isDouble)) { + type = "num"; + suffix = p.isDouble ? ".toDouble()" : ".toInt()"; + } else if (p.isList || p.isSet) { + type = "Iterable"; - this.error = this.checkJson(); - } - - async checkJson() { - const isArray = this.json.startsWith('['); - if (isArray && !this.json.includes('{')) { - return 'Primitive JSON arrays are not supported! Please serialize them directly.'; + if (p.subtype.isMap) { + suffix = `.map((x) => ${p.subtype.rawType}.from(x as Map))`; + } else if (p.subtype.isCollection) { + suffix = `.map((x) => ${p.subtype.rawType}.from(x as Iterable))`; } + } else if (p.isMap) { + type = "Map"; + } - if (await this.generateFiles()) { - return 'The provided JSON is malformed or couldn\'t be parsed!'; - } + let nullable = + (p.isNullable || withDefaultValues) && !p.hasNullCheck ? "?" : ""; + type += nullable; + + // Adjust suffix for nullable types + suffix = nullable && suffix ? "?" + suffix : suffix; - return null; + return [type, suffix]; } /** - * @param {string} source + * @param {ClassField} p + * @param {string} customType */ - toPlainJson(source) { - return source.replace(new RegExp(' ', 'g'), '').replace(new RegExp('\n', 'g'), ''); + function cast(p, customType = null) { + const [type, suffix] = retype(p); + return `cast<${customType ?? type}>('${p.key}')${suffix}`; } /** - * @param {any} value + * @param {ClassField} p */ - getPrimitive(value) { - let type = typeof (value); - let sType = null; - - if (type === 'number') { - sType = Number.isInteger(value) ? 'int' : 'double'; - } else if (type === 'string') { - sType = 'String' - } else if (type === 'boolean') { - sType = 'bool'; - } + function customTypeMapping(p) { + function defVal(value) { + if (!value) return ""; + return withDefaultValues && !p.isNullable ? ` ?? ${value}` : ""; + } - return sType; - } + const typeSetting = readCustomTypeSetting(p.type); - /** - * Create DartClasses from a JSON mapping with class content and properties. - * This is intended only for creating new files not overriding exisiting ones. - * - * @param {any} object - * @param {string} key - */ - getClazzes(object, key) { - let clazz = new DartClass(); - clazz.startsAtLine = 1; - clazz.name = capitalize(key); - - let isArray = false; - if (object instanceof Array) { - isArray = true; - clazz.isArray = true; - clazz.name += 's'; - } else { - // Top level arrays are currently not supported! - this.clazzes.push(clazz); + if (typeSetting) { + if (typeSetting.fromMap === "") { + return cast(p); } - let i = 1; - clazz.classContent += 'class ' + clazz.name + ' {\n'; - for (let key in object) { - // named key for class names. - let k = !isArray ? key : removeEnd(clazz.name.toLowerCase(), 's'); - - let value = object[key]; - let type = this.getPrimitive(value); - - if (type == null) { - if (value instanceof Array) { - if (value.length > 0) { - let listType = k; - // Adjust the class name of lists. E.g. a key with items - // becomes a class name of Item. - if (k.endsWith('ies')) listType = removeEnd(k, 'ies') + 'y'; - if (k.endsWith('s')) listType = removeEnd(k, 's'); - const i0 = this.getPrimitive(value[0]); - - if (i0 == null) { - this.getClazzes(value[0], listType); - type = 'List<' + capitalize(listType) + '>'; - } else { - type = 'List<' + i0 + '>'; - } - } else { - type = 'List'; - } - } else { - this.getClazzes(value, k); - type = !isArray ? capitalize(k) : `List<${capitalize(k)}>`; - } - } + const [from, open1, typedef, close1] = extractFromMap( + typeSetting.fromMap + ); + const [stype, def] = typedef.split("??").map((i) => (i ?? "").trim()); - clazz.properties.push(new ClassField(type, k, ++i, true, false, true)); - clazz.classContent += ` final ${type} ${toVarName(k)};\n`; + const hasDef = (def ?? "") !== "" && !p.hasNullCheck; + const defValue = hasDef ? ` ?? ${def}` : ""; - // If object is JSONArray, break after first item. - if (isArray) break; - } - clazz.endsAtLine = ++i; - clazz.classContent += '}'; - } + const type = stype === "" ? p.type : stype; + const dot = from === "" ? "" : `.${from}`; - /** - * @param {string} property - */ - getGeneratedTypeCount(property) { - let p = new ClassField(property, 'x'); - let i = 0; - if (!p.isPrimitive) { - for (let clazz of this.clazzes) { - if (clazz.name == p.rawType) { - i++; - } - } - } + const withDefaultValues = readSetting("fromMap.default_values"); - return i; - } + if (p.isSubtype) { + const inclass = new ClassField(stype, "type", p.line); + let [intype, suffix] = retype(inclass); - async generateFiles() { - try { - const json = JSON.parse(this.json); - this.getClazzes(json, this.clazzName); - this.removeDuplicates(); + // we add brackets if we have a suffix + const [open2, close2] = suffix === "" ? ["", ""] : ["(", ")"]; - for (let clazz of this.clazzes) { - this.files.push(new DartFile(clazz)); - } + if (withDefaultValues) { + suffix += ` ?? ${inclass.defValue}`; + } - return false; - } catch (e) { - console.log(e.msg); - return true; + return `${p.type}${dot}${open1}${open2}x as ${intype}${ + hasDef ? "? ??" : "" + }${defValue}${close2}${suffix}${close1}`; } - } - // If multiple clazzes of the same class exist, remove the duplicates - // before writing them. - removeDuplicates() { - let result = []; - let clazzes = this.clazzes.map((item) => item.classContent); - clazzes.forEach((item, index) => { - if (clazzes.indexOf(item) == index) { - result.push(this.clazzes[index]); - } - }); + return `${p.type}${dot}${open1}cast<${type}${hasDef ? "?" : ""}>('${ + p.key + }')${defValue}${close1}`; + } - this.clazzes = result; + if (p.isSubtype) { + return `${p.type}.fromMap(Map.from(x as Map))`; + } + return `${p.type}.fromMap(Map.from(${cast(p, "Map")}${defVal("{}")}))`; } - /** - * @param {DataClassGenerator} generator - */ - addGeneratedFilesAsImport(generator) { - const clazz = generator.clazzes[0]; - for (let prop of clazz.properties) { - // Import only inambigous generated types. - // E.g. if there are multiple generated classes with - // the same name, do not include an import of that class. - if (this.getGeneratedTypeCount((prop.subtype ?? prop).rawType) == 1) { - const imp = `import '${createFileName((prop.subtype ?? prop).rawType)}.dart';`; - generator.imports.push(imp); - } - } - } + const customError = readSetting("custom.argumentError"); - /** - * @param {vscode.Progress} progress - * @param {boolean} separate - */ - async commitJson(progress, separate) { - let path = getCurrentPath(); - let fileContent = ''; + let method = `factory ${clazz.name}.fromMap(Map map) {\n`; + method += ` T cast(String k) => map[k] is T ? map[k] as T : ${customError}\n`; + method += " return " + clazz.type + "(\n"; + for (let p of props) { + method += ` ${clazz.hasNamedConstructor ? `${p.name}: ` : ""}`; - const length = this.files.length; - for (let i = 0; i < length; i++) { - const file = this.files[i]; - const isLast = i == length - 1; - const generator = new DataClassGenerator(file.content, [file.clazz], true); + // const value = `cast<${p.base}>('${p}')`; + const value = cast(p); + const hasNullCheck = p.hasNullCheck; - if (separate) - this.addGeneratedFilesAsImport(generator) + if (hasNullCheck) { + method += `map['${p.key}'] != null ? `; + } - const imports = `${generator.imports.formatted}\n`; + function defVal(value) { + return withDefaultValues && !p.isNullable ? ` ?? ${value}` : ""; + } - progress.report({ - increment: ((1 / length) * 100), - message: `Creating file ${file.name}...` - }); + // custom serialization + if (p.ignore) { + method += `cast<${p.rawType}>('${p.key}')`; + } else if (p.isCustomFrom) { + const [from, open, typedef, close] = p.fromCustom; + const [type, def] = typedef.split("??").map((i) => (i ?? "").trim()); - if (separate) { - const clazz = generator.clazzes[0]; + const hasDef = (def ?? "") !== "" && !p.hasNullCheck; + const putDef = hasDef ? ` ?? ${def}` : ""; - const replacement = imports + clazz.generateClassReplacement(); - if (i > 0) { - await writeFile(replacement, file.name, false, path); - } else { - await getEditor().edit(editor => { - editorReplace(editor, 0, null, replacement); - }); - } + method += `${p.type}.${from}${open}cast<${type}${hasDef ? "?" : ""}>('${ + p.key + }')${putDef}${close}`; - // Slow the writing process intentionally down. - await new Promise(resolve => setTimeout(() => resolve(), 120)); - } else { - // Insert in current file when JSON should not be separated. - for (let clazz of generator.clazzes) { - fileContent += clazz.generateClassReplacement() + '\n\n'; + // serialization + } else if (p.isEnum) { + const setting = readSetting("json.enum_format"); + const evalues = p.type + ".values"; + + if (setting === "byIndex") { + p.rawType = "int"; + method += `${evalues}[${cast(p)}${defVal("0")}]`; + } else { + p.rawType = "String"; + method += `${evalues}.byName(${cast(p)}${defVal( + `${evalues}.first.name` + )})`; + } + } else if (p.isCollection) { + let listSubtype = p.type.match(/<(.+?)>/)[1]; + if (listSubtype.startsWith("Map")) listSubtype = listSubtype + ">"; + + const defaultValue = + withDefaultValues && !p.isNullable + ? ` ?? const ${ + p.isMap + ? "{}" + : p.isList + ? `<${listSubtype}>[]` + : `<${listSubtype}>{}` + }` + : ""; + + method += `${p.type}.from(`; + if (p.subtype.isPrimitive || p.subtype.isCollection) { + method += `${value}${defaultValue})`; + } else { + const qm = defaultValue === "" ? "" : "?"; + method += `cast('${ + p.key + }')${qm}.map((x) => ${customTypeMapping(p.subtype)})${defaultValue})`; + } + } else if (p.isPrimitive) { + const defaultValue = + withDefaultValues && !p.isNullable ? ` ?? ${p.defValue} ` : ""; + method += `${value}${defaultValue}`; + } else { + method += customTypeMapping(p); + } + + if (hasNullCheck) { + method += ` : null`; + } + + method += ",\n"; + + const isLast = p.name == props[props.length - 1].name; + if (isLast) method += " );\n"; + } + method += "}"; + + this.appendOrReplace( + "fromMap", + method, + `factory ${clazz.name}.fromMap(Map map)`, + clazz + ); + } + + /** + * @param {DartClass} clazz + */ + insertToJson(clazz) { + this.requiresImport("dart:convert"); + + const method = "String toJson() => json.encode(toMap());"; + this.appendOrReplace("toJson", method, "String toJson()", clazz); + } + + /** + * @param {DartClass} clazz + */ + insertFromJson(clazz) { + this.requiresImport("dart:convert"); + + const method = `factory ${clazz.name}.fromJson(String source) => ${clazz.name}.fromMap(json.decode(source) as Map);`; + this.appendOrReplace( + "fromJson", + method, + `factory ${clazz.name}.fromJson(String source)`, + clazz + ); + } + + /** + * @param {DartClass} clazz + */ + insertToString(clazz) { + const short = clazz.fewProps; + const props = clazz.properties; + let method = "@override\n"; + method += `String toString() ${short ? "=>" : "{\n"}`; + method += `${short ? "" : " return "}'''${clazz.name}(\n`; + + for (let p of props) { + const name = p.name; + method += ` ${name}: $${name},\n`; + } + + method += " )'''"; + method += short ? ";" : ";\n}"; + + this.appendOrReplace("toString", method, "String toString()", clazz); + } + + /** + * @param {DartClass} clazz + */ + insertEquality(clazz) { + const props = clazz.properties; + const hasCollection = props.find((p) => p.isCollection) != undefined; + + let collectionEqualityFn; + if (hasCollection) { + // Flutter already has collection equality functions + // in the foundation package. + if (isFlutter) { + this.requiresImport("package:flutter/foundation.dart"); + } else { + this.requiresImport("package:collection/collection.dart"); + + collectionEqualityFn = "collectionEquals"; + const isListOnly = + props.find((p) => p.isCollection && !p.isList) == undefined; + if (isListOnly) collectionEqualityFn = "listEquals"; + const isMapOnly = + props.find((p) => p.isCollection && !p.isMap) == undefined; + if (isMapOnly) collectionEqualityFn = "mapEquals"; + const isSetOnly = + props.find((p) => p.isCollection && !p.isSet) == undefined; + if (isSetOnly) collectionEqualityFn = "setEquals"; + } + } + + let method = "@override\n"; + method += "bool operator ==(Object other) {\n"; + method += " if (identical(this, other)) return true;\n"; + if (hasCollection && !isFlutter) + method += ` final ${collectionEqualityFn} = const DeepCollectionEquality().equals;\n`; + method += "\n"; + method += " return other is " + clazz.type + " &&\n"; + for (let prop of props) { + if (prop.isCollection) { + if (isFlutter) + collectionEqualityFn = prop.isSet + ? "setEquals" + : prop.isMap + ? "mapEquals" + : "listEquals"; + method += ` ${collectionEqualityFn}(other.${prop.name}, ${prop.name})`; + } else { + method += ` other.${prop.name} == ${prop.name}`; + } + if (prop.name != props[props.length - 1].name) method += " &&\n"; + else method += ";\n"; + } + method += "}"; + + this.appendOrReplace("equality", method, "bool operator ==", clazz); + } + + /** + * @param {DartClass} clazz + */ + insertHash(clazz) { + const useJenkins = readSetting("hashCode.use_jenkins"); + const short = !useJenkins && clazz.fewProps; + const props = clazz.properties; + let method = "@override\n"; + method += `int get hashCode ${short ? "=>" : "{\n return "}`; + + if (useJenkins) { + // dart:ui import is required for Jenkins hash. + this.requiresImport("dart:ui", [ + "package:flutter/material.dart", + "package:flutter/cupertino.dart", + "package:flutter/widgets.dart", + ]); + + method += `hashList([\n`; + for (let p of props) { + method += " " + p.name + `,\n`; + } + method += " ]);"; + } else { + for (let p of props) { + const isFirst = p == props[0]; + method += `${isFirst && !short ? "" : short ? " " : " "}${ + p.name + }.hashCode`; + if (p == props[props.length - 1]) { + method += ";"; + } else { + method += ` ^${!short ? "\n" : ""}`; + } + } + } + + if (!short) method += "\n}"; + + this.appendOrReplace("hashCode", method, "int get hashCode", clazz); + } + + /** + * @param {DartClass} clazz + */ + addEquatableDetails(clazz) { + // Do not generate Equatable for class with 'Base' in their + // names as Base classes should inherit from Equatable. + // see: https://github.com/arthurbcd/dart-safe-data-class-generator/issues/8 + if (clazz.hasSuperclass && clazz.superclass.includes("Base")) return; + + this.requiresImport("package:equatable/equatable.dart"); + + if (!clazz.usesEquatable) { + if (clazz.hasSuperclass) { + this.addMixin("EquatableMixin"); + } else { + this.setSuperClass("Equatable"); + } + } + } + + /** + * @param {DartClass} clazz + */ + insertEquatable(clazz) { + this.addEquatableDetails(clazz); + + const props = clazz.properties; + const short = props.length <= 4; + const split = short ? ", " : ",\n"; + let method = "@override\n"; + method += `List get props ${!short ? "{\n" : "=>"}`; + method += `${!short ? " return" : ""} ` + "[" + (!short ? "\n" : ""); + for (let prop of props) { + const isLast = prop.name == props[props.length - 1].name; + const inset = !short ? " " : ""; + method += inset + prop.name + split; + + if (isLast) { + if (short) method = removeEnd(method, split); + method += (!short ? " " : "") + "];" + (!short ? "\n" : ""); + } + } + method += !short ? "}" : ""; + + this.appendOrReplace("props", method, "List get props", clazz); + } + + /** + * @param {string} mixin + */ + addMixin(mixin) { + const mixins = this.clazz.mixins; + if (!mixins.includes(mixin)) { + mixins.push(mixin); + } + } + + /** + * @param {string} impl + */ + addInterface(impl) { + const interfaces = this.clazz.interfaces; + if (!interfaces.includes(impl)) { + interfaces.push(impl); + } + } + + /** + * @param {string} clazz + */ + setSuperClass(clazz) { + this.clazz.superclass = clazz; + } + + /** + * @param {string} name + * @param {string} n + * @param {string} finder + * @param {DartClass} clazz + */ + appendOrReplace(name, n, finder, clazz) { + let part = this.findPart(name, finder, clazz); + let replacement = removeEnd(indent(n.replace("@override\n", "")), "\n"); + + if (part != null) { + part.replacement = replacement; + if (!areStrictEqual(part.current, part.replacement)) { + this.replace(part, clazz); + } + } else { + this.append(n, clazz); + } + } + + /** + * @param {string} method + * @param {DartClass} clazz + */ + append(method, clazz, constr = false) { + let met = indent(method); + constr ? (clazz.constr = met) : (clazz.toInsert += "\n" + met); + } + + /** + * @param {ClassPart} part + * @param {DartClass} clazz + */ + replace(part, clazz) { + clazz.toReplace.push(part); + } + + parseAndReadClasses() { + let clazzes = []; + let clazz = new DartClass(); + + let lines = this.text.split("\n"); + let curlyBrackets = 0; + let brackets = 0; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const linePos = i + 1; + // Make sure to look for 'class ' with the space in order to allow + // fields that contain the word 'class' as in classifire. + // issue: https://github.com/arthurbcd/dart-data-class-tools/issues/2 + + const prefixes = [ + "class ", + "abstract class ", + "sealed class ", + "final class ", + ]; + + function testPrefix(prefix) { + const found = line.trim().startsWith(prefix); + if (found) { + clazz.classType = prefix.trim(); + return true; + } + return false; + } + + const classLine = prefixes.some(testPrefix); + + // const classLine = line.trimLeft().startsWith('class ') || line.trimLeft().startsWith('abstract class ') || line.trimLeft().startsWith('sealed class '); + + if (classLine) { + // clazz = new DartClass(); + clazz.startsAtLine = linePos; + + if (lines[linePos - 2].includes("@immutable")) { + clazz.hasImmutableAnnotation = true; + } + + let classNext = false; + let extendsNext = false; + let implementsNext = false; + let mixinsNext = false; + + // Reset brackets count when a new class was detected. + curlyBrackets = 0; + brackets = 0; + + const words = this.splitWhileMaintaingGenerics(line); + for (let word of words) { + word = word.trim(); + if (word.length > 0) { + if (word == "class") { + classNext = true; + } else if (word == "extends") { + extendsNext = true; + } else if (extendsNext) { + extendsNext = false; + clazz.superclass = word; + } else if (word == "with") { + mixinsNext = true; + extendsNext = false; + implementsNext = false; + } else if (word == "implements") { + mixinsNext = false; + extendsNext = false; + implementsNext = true; + } else if (classNext) { + classNext = false; + + // Remove generics from class name. + if (word.includes("<")) { + clazz.fullGenericType = word.substring( + word.indexOf("<"), + word.lastIndexOf(">") + 1 + ); + + word = word.substring(0, word.indexOf("<")); + } + + clazz.name = word; + } else if (mixinsNext) { + const mixin = removeEnd(word, ",").trim(); + + if (mixin.length > 0) { + clazz.mixins.push(mixin); + } + } else if (implementsNext) { + const impl = removeEnd(word, ",").trim(); + + if (impl.length > 0) { + clazz.interfaces.push(impl); + } + } + } + } + + // Do not add State classes of widgets. + if (!clazz.isState) { + clazzes.push(clazz); + } + } + + if (clazz.classDetected) { + // Check if class ended based on bracket count. If all '{' have a '}' pair, + // class can be closed. + curlyBrackets += count(line, "{"); + curlyBrackets -= count(line, "}"); + // Count brackets, e.g. to find the constructor. + brackets += count(line, "("); + brackets -= count(line, ")"); + + // Detect beginning of constructor by looking for the class name and a bracket, while also + // making sure not to falsely detect a function constructor invocation with the actual + // constructor with boilerplaty checking all possible constructor options. + const includesConstr = line + .replace("const", "") + .trimLeft() + .startsWith(clazz.name + "("); + if (includesConstr && !classLine) { + clazz.constrStartsAtLine = linePos; + } + + if ( + clazz.constrStartsAtLine != null && + clazz.constrEndsAtLine == null + ) { + clazz.constr = + clazz.constr == null ? line + "\n" : clazz.constr + line + "\n"; + + // Detect end of constructor. + if (brackets == 0) { + clazz.constrEndsAtLine = linePos; + clazz.constr = removeEnd(clazz.constr, "\n"); + } + } + + clazz.classContent += line; + // Detect end of class. + if (curlyBrackets != 0) { + clazz.classContent += "\n"; + } else { + clazz.endsAtLine = linePos; + clazz = new DartClass(); + } + + if (brackets == 0 && curlyBrackets == 1) { + // Check if a line is valid to only include real properties. + const lineValid = + // Line shouldn't start with the class name as this would + // be the constructor or an error. + !line.trimLeft().startsWith(clazz.name) && + // Ignore comments. + !line.trimLeft().startsWith("//") && + // These symbols would indicate that this is not a field. + !includesOne(line, ["{", "}", "=>", "@"], false) && + // Filter out some keywords. + !includesOne(line, ["static", "set", "get", "return", "factory"]) && + // Do not include final values that are assigned a value. + !includesAll(line, ["final ", "="]) && + // Do not inlcude non final fields that were declared after the constructor. + (clazz.constrStartsAtLine == null || line.includes("final ")) && + // Make sure not to catch abstract functions. + !line.replace(/\s/g, "").endsWith(");"); + + if (lineValid) { + let type = null; + let name = null; + let isFinal = false; + let isConst = false; + + const words = line.trim().split(" "); + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const isLast = i == words.length - 1; + + if (word.length > 0 && word != "}" && word != "{") { + if (word == "final") { + isFinal = true; + } else if (i == 0 && word == "const") { + isConst = true; } - if (isLast) { - fileContent = removeEnd(fileContent, '\n\n'); - await getEditor().edit(editor => { - editorReplace(editor, 0, null, fileContent); - editorInsert(editor, 0, imports); - }); + // Be sure to not include keywords. + if (word != "final" && word != "const") { + // If word ends with semicolon => variable name, else type. + let isVariable = + word.endsWith(";") || (!isLast && words[i + 1] == "="); + // Make sure we don't capture abstract functions like: String func(); + isVariable = isVariable && !includesOne(word, ["(", ")"]); + if (isVariable) { + if (name == null) name = removeEnd(word, ";"); + } else { + if (type == null) type = word; + // Types can have gaps => Pair, + // thus append word to type if a name hasn't + // been detected. + else if (name == null) type += " " + word; + } } + } } - } - } -} - -class DataClassCodeActions { - constructor() { - this.clazz = new DartClass(); - this.generator = null; - this.document = getDoc(); - this.line = ''; - this.range; - } - get uri() { - return this.document.uri; - } + if (type != null && name != null) { + const prop = new ClassField( + type, + name, + linePos, + isFinal, + isConst + ); - get lineNumber() { - return this.range.start.line + 1; - } + if (i > 0) { + const prevLine = lines[i - 1]; - get charPos() { - return this.range.start.character; - } + // Ignore comments, search for directives. + const commentParts = lines[i].split("//"); + const directives = + commentParts.length > 1 ? commentParts[1].trim() : ""; - /** - * @param {vscode.TextDocument} document - * @param {vscode.Range} range - */ - provideCodeActions(document, range) { - if (!readSetting('quick_fixes')) { - return; - } + // Check if is Enum. - this.range = range; - this.document = document; - this.line = document.lineAt(range.start).text; - this.generator = new DataClassGenerator(document.getText()); - this.clazz = this.getClass(); + prop.ignore = !!(directives === "ignore"); + prop.isEnum = !!( + prevLine.match(/^\s*\/\/(\s*)enum/) || directives === "enum" + ); - // * Class independent code actions. - const codeActions = [ - this.createImportsFix(), - ]; + // Custom serialization. + const [from, to] = directives.split(",").map((i) => i.trim()); - if (this.clazz == null || !this.clazz.isValid) { - return codeActions; - } + if (from !== "") { + // // fromCustom(type ?? def) || fromCustom[type ?? def] + // const regex = /(\w+)([\[(])((?:[^()\[\]]|\((?:[^()]*\)))*)((?:[\])]))/; - const line = this.lineNumber; - const clazz = this.clazz; - const isAtClassDeclaration = line == clazz.startsAtLine; - const isInProperties = clazz.properties.find((p) => p.line == line) != undefined; - const isInConstrRange = line >= clazz.constrStartsAtLine && line <= clazz.constrEndsAtLine; - if (!(isAtClassDeclaration || isInProperties || isInConstrRange)) return codeActions; - - // * Class code actions. - if (!this.clazz.isWidget) - codeActions.push(this.createDataClassFix(this.clazz)); - - if (readSetting('constructor.enabled')) - codeActions.push(this.createConstructorFix()); - - // Only add constructor fix for widget classes. - if (!this.clazz.isWidget) { - // Copy with and JSON serialization should be handled by - // subclasses. - if (!this.clazz.isAbstract) { - if (readSetting('copyWith.enabled')) - codeActions.push(this.createCopyWithFix()); - if (readSettings(['toMap.enabled', 'fromMap.enabled', 'toJson.enabled', 'fromJson.enabled'])) - codeActions.push(this.createSerializationFix()); - } + // extractFromMap(fromMap) - if (readSetting('toString.enabled')) - codeActions.push(this.createToStringFix()); + // const r = regex.exec(from); + prop.fromCustom = extractFromMap(from); + } + if (to !== "") prop.toCustom = to ?? ""; + } - if (clazz.usesEquatable || readSetting('useEquatable')) - codeActions.push(this.createUseEquatableFix()); - else { - if (readSettings(['equality.enabled', 'hashCode.enabled'])) - codeActions.push(this.createEqualityFix()); + clazz.properties.push(prop); } + } } - - return codeActions; + } } - /** - * @param {string} description - * @param {(arg0: vscode.WorkspaceEdit) => void} editor - */ - createFix(description, editor) { - const fix = new vscode.CodeAction(description, vscode.CodeActionKind.QuickFix); - const edit = new vscode.WorkspaceEdit(); - editor(edit); - fix.edit = edit; - return fix; - } + return clazzes; + } - /** - * @param {DartClass} clazz - */ - createDataClassFix(clazz) { - if (clazz.didChange) { - const fix = new vscode.CodeAction('Generate data class', vscode.CodeActionKind.QuickFix); - fix.edit = this.getClazzEdit(clazz); - return fix; + /** + * This function is for parsing the class name line while maintaining + * also more complex generic types like class A>. + * + * @param {string} line + */ + splitWhileMaintaingGenerics(line) { + let words = []; + let index = 0; + let generics = 0; + for (let i = 0; i < line.length; i++) { + const char = line[i]; + const isCurly = char == "{"; + const isSpace = char == " "; + + if (char == "<") generics++; + if (char == ">") generics--; + + if (generics == 0 && (isSpace || isCurly)) { + const word = line.substring(index, i).trim(); + + // Do not add whitespace. + if (word.length == 0) continue; + const isOnlyGeneric = word.startsWith("<"); + + // Append the generic type to the word when there is spacing + // between them. E.g.: class Hello + if (isOnlyGeneric) { + words[words.length - 1] = words[words.length - 1] + word; + } else { + words.push(word); } - } - /** - * @param {string} part - * @param {string} description - */ - constructQuickFix(part, description) { - const generator = new DataClassGenerator(this.document.getText(), null, false, part); - const fix = new vscode.CodeAction(description, vscode.CodeActionKind.QuickFix); - const clazz = this.findQuickFixClazz(generator); - if (clazz != null && clazz.didChange) { - fix.edit = this.getClazzEdit(clazz, generator.imports); - return fix; + if (isCurly) { + break; } + + index = i; + } } - /** @param {DataClassGenerator} generator */ - findQuickFixClazz(generator) { - for (let clazz of generator.clazzes) { - if (clazz.name == this.clazz.name) - return clazz; + return words; + } +} + +class DartFile { + /** + * @param {DartClass} clazz + * @param {string} content + */ + constructor(clazz, content = null) { + this.clazz = clazz; + this.name = createFileName(clazz.name); + this.content = content || clazz.classContent; + } +} + +class JsonReader { + /** + * @param {string} source + * @param {string} className + */ + constructor(source, className) { + this.json = this.toPlainJson(source); + + this.clazzName = capitalize(className); + /** @type {DartClass[]} */ + this.clazzes = []; + /** @type {DartFile[]} */ + this.files = []; + + this.error = this.checkJson(); + } + + async checkJson() { + const isArray = this.json.startsWith("["); + if (isArray && !this.json.includes("{")) { + return "Primitive JSON arrays are not supported! Please serialize them directly."; + } + + if (await this.generateFiles()) { + return "The provided JSON is malformed or couldn't be parsed!"; + } + + return null; + } + + /** + * @param {string} source + */ + toPlainJson(source) { + return source + .replace(new RegExp(" ", "g"), "") + .replace(new RegExp("\n", "g"), ""); + } + + /** + * @param {any} value + */ + getPrimitive(value) { + let type = typeof value; + let sType = null; + + if (type === "number") { + sType = Number.isInteger(value) ? "int" : "double"; + } else if (type === "string") { + sType = "String"; + } else if (type === "boolean") { + sType = "bool"; + } + + return sType; + } + + /** + * Create DartClasses from a JSON mapping with class content and properties. + * This is intended only for creating new files not overriding exisiting ones. + * + * @param {any} object + * @param {string} key + */ + getClazzes(object, key) { + let clazz = new DartClass(); + clazz.startsAtLine = 1; + clazz.name = capitalize(key); + + let isArray = false; + if (object instanceof Array) { + isArray = true; + clazz.isArray = true; + clazz.name += "s"; + } else { + // Top level arrays are currently not supported! + this.clazzes.push(clazz); + } + + let i = 1; + clazz.classContent += "class " + clazz.name + " {\n"; + for (let key in object) { + // named key for class names. + let k = !isArray ? key : removeEnd(clazz.name.toLowerCase(), "s"); + + let value = object[key]; + let type = this.getPrimitive(value); + + if (type == null) { + if (value instanceof Array) { + if (value.length > 0) { + let listType = k; + // Adjust the class name of lists. E.g. a key with items + // becomes a class name of Item. + if (k.endsWith("ies")) listType = removeEnd(k, "ies") + "y"; + if (k.endsWith("s")) listType = removeEnd(k, "s"); + const i0 = this.getPrimitive(value[0]); + + if (i0 == null) { + this.getClazzes(value[0], listType); + type = "List<" + capitalize(listType) + ">"; + } else { + type = "List<" + i0 + ">"; + } + } else { + type = "List"; + } + } else { + this.getClazzes(value, k); + type = !isArray ? capitalize(k) : `List<${capitalize(k)}>`; } - } + } - /** - * @param {DartClass} clazz - */ - getClazzEdit(clazz, imports = null) { - return getReplaceEdit(clazz, imports || this.generator.imports); - } + clazz.properties.push(new ClassField(type, k, ++i, true, false, true)); + clazz.classContent += ` final ${type} ${toVarName(k)};\n`; - createConstructorFix() { - return this.constructQuickFix('constructor', 'Generate constructor'); + // If object is JSONArray, break after first item. + if (isArray) break; } + clazz.endsAtLine = ++i; + clazz.classContent += "}"; + } - createCopyWithFix() { - return this.constructQuickFix('copyWith', 'Generate copyWith'); + /** + * @param {string} property + */ + getGeneratedTypeCount(property) { + let p = new ClassField(property, "x"); + let i = 0; + if (!p.isPrimitive) { + for (let clazz of this.clazzes) { + if (clazz.name == p.rawType) { + i++; + } + } } - createSerializationFix() { - return this.constructQuickFix('serialization', 'Generate JSON serialization'); - } + return i; + } - createToStringFix() { - return this.constructQuickFix('toString', 'Generate toString'); - } + async generateFiles() { + try { + const json = JSON.parse(this.json); + this.getClazzes(json, this.clazzName); + this.removeDuplicates(); - createEqualityFix() { - return this.constructQuickFix('equality', 'Generate equality'); - } + for (let clazz of this.clazzes) { + this.files.push(new DartFile(clazz)); + } - createUseEquatableFix() { - return this.constructQuickFix('useEquatable', `Generate Equatable`); + return false; + } catch (e) { + console.log(e.msg); + return true; } + } + + // If multiple clazzes of the same class exist, remove the duplicates + // before writing them. + removeDuplicates() { + let result = []; + let clazzes = this.clazzes.map((item) => item.classContent); + clazzes.forEach((item, index) => { + if (clazzes.indexOf(item) == index) { + result.push(this.clazzes[index]); + } + }); - createImportsFix() { - const imports = new Imports(this.document.getText()); - if (!imports.didChange) return; + this.clazzes = result; + } + + /** + * @param {DataClassGenerator} generator + */ + addGeneratedFilesAsImport(generator) { + const clazz = generator.clazzes[0]; + for (let prop of clazz.properties) { + // Import only inambigous generated types. + // E.g. if there are multiple generated classes with + // the same name, do not include an import of that class. + if (this.getGeneratedTypeCount((prop.subtype ?? prop).rawType) == 1) { + const imp = `import '${createFileName( + (prop.subtype ?? prop).rawType + )}.dart';`; + generator.imports.push(imp); + } + } + } + + /** + * @param {vscode.Progress} progress + * @param {boolean} separate + */ + async commitJson(progress, separate) { + let path = getCurrentPath(); + let fileContent = ""; + + const length = this.files.length; + for (let i = 0; i < length; i++) { + const file = this.files[i]; + const isLast = i == length - 1; + const generator = new DataClassGenerator( + file.content, + [file.clazz], + true + ); + + if (separate) this.addGeneratedFilesAsImport(generator); + + const imports = `${generator.imports.formatted}\n`; + + progress.report({ + increment: (1 / length) * 100, + message: `Creating file ${file.name}...`, + }); + + if (separate) { + const clazz = generator.clazzes[0]; - const inImportsRange = this.lineNumber >= imports.startAtLine && this.lineNumber <= imports.endAtLine; - if (inImportsRange) { - let title = 'Sort imports'; - if (imports.hasImportDeclaration && imports.hasExportDeclaration) { - title = 'Sort imports/exports'; - } else if (imports.hasExportDeclaration) { - title = 'Sort exports'; - } + const replacement = imports + clazz.generateClassReplacement(); + if (i > 0) { + await writeFile(replacement, file.name, false, path); + } else { + await getEditor().edit((editor) => { + editorReplace(editor, 0, null, replacement); + }); + } - return this.createFix(title, (edit) => { - edit.replace(this.uri, imports.range, imports.formatted); - }); + // Slow the writing process intentionally down. + await new Promise((resolve) => setTimeout(() => resolve(), 120)); + } else { + // Insert in current file when JSON should not be separated. + for (let clazz of generator.clazzes) { + fileContent += clazz.generateClassReplacement() + "\n\n"; } - } - getClass() { - for (let clazz of this.generator.clazzes) { - if (clazz.startsAtLine <= this.lineNumber && clazz.endsAtLine >= this.lineNumber) { - return clazz; - } + if (isLast) { + fileContent = removeEnd(fileContent, "\n\n"); + await getEditor().edit((editor) => { + editorReplace(editor, 0, null, fileContent); + editorInsert(editor, 0, imports); + }); } + } } + } +} + +class DataClassCodeActions { + constructor() { + this.clazz = new DartClass(); + this.generator = null; + this.document = getDoc(); + this.line = ""; + this.range; + } + + get uri() { + return this.document.uri; + } + + get lineNumber() { + return this.range.start.line + 1; + } + + get charPos() { + return this.range.start.character; + } + + /** + * @param {vscode.TextDocument} document + * @param {vscode.Range} range + */ + provideCodeActions(document, range) { + if (!readSetting("quick_fixes")) { + return; + } + + this.range = range; + this.document = document; + this.line = document.lineAt(range.start).text; + this.generator = new DataClassGenerator(document.getText()); + this.clazz = this.getClass(); + + // * Class independent code actions. + const codeActions = [this.createImportsFix()]; + + if (this.clazz == null || !this.clazz.isValid) { + return codeActions; + } + + const line = this.lineNumber; + const clazz = this.clazz; + const isAtClassDeclaration = line == clazz.startsAtLine; + const isInProperties = + clazz.properties.find((p) => p.line == line) != undefined; + const isInConstrRange = + line >= clazz.constrStartsAtLine && line <= clazz.constrEndsAtLine; + if (!(isAtClassDeclaration || isInProperties || isInConstrRange)) + return codeActions; + + // * Class code actions. + if (!this.clazz.isWidget) + codeActions.push(this.createDataClassFix(this.clazz)); + + if (readSetting("constructor.enabled")) + codeActions.push(this.createConstructorFix()); + + // Only add constructor fix for widget classes. + if (!this.clazz.isWidget) { + // Copy with and JSON serialization should be handled by + // subclasses. + if (!this.clazz.isAbstract) { + if (readSetting("copyWith.enabled")) + codeActions.push(this.createCopyWithFix()); + if ( + readSettings([ + "toMap.enabled", + "fromMap.enabled", + "toJson.enabled", + "fromJson.enabled", + ]) + ) + codeActions.push(this.createSerializationFix()); + } + + if (readSetting("toString.enabled")) + codeActions.push(this.createToStringFix()); + + if (clazz.usesEquatable || readSetting("useEquatable")) + codeActions.push(this.createUseEquatableFix()); + else { + if (readSettings(["equality.enabled", "hashCode.enabled"])) + codeActions.push(this.createEqualityFix()); + } + } + + return codeActions; + } + + /** + * @param {string} description + * @param {(arg0: vscode.WorkspaceEdit) => void} editor + */ + createFix(description, editor) { + const fix = new vscode.CodeAction( + description, + vscode.CodeActionKind.QuickFix + ); + const edit = new vscode.WorkspaceEdit(); + editor(edit); + fix.edit = edit; + return fix; + } + + /** + * @param {DartClass} clazz + */ + createDataClassFix(clazz) { + if (clazz.didChange) { + const fix = new vscode.CodeAction( + "Generate data class", + vscode.CodeActionKind.QuickFix + ); + fix.edit = this.getClazzEdit(clazz); + return fix; + } + } + + /** + * @param {string} part + * @param {string} description + */ + constructQuickFix(part, description) { + const generator = new DataClassGenerator( + this.document.getText(), + null, + false, + part + ); + const fix = new vscode.CodeAction( + description, + vscode.CodeActionKind.QuickFix + ); + const clazz = this.findQuickFixClazz(generator); + if (clazz != null && clazz.didChange) { + fix.edit = this.getClazzEdit(clazz, generator.imports); + return fix; + } + } + + /** @param {DataClassGenerator} generator */ + findQuickFixClazz(generator) { + for (let clazz of generator.clazzes) { + if (clazz.name == this.clazz.name) return clazz; + } + } + + /** + * @param {DartClass} clazz + */ + getClazzEdit(clazz, imports = null) { + return getReplaceEdit(clazz, imports || this.generator.imports); + } + + createConstructorFix() { + return this.constructQuickFix("constructor", "Generate constructor"); + } + + createCopyWithFix() { + return this.constructQuickFix("copyWith", "Generate copyWith"); + } + + createSerializationFix() { + return this.constructQuickFix( + "serialization", + "Generate JSON serialization" + ); + } + + createToStringFix() { + return this.constructQuickFix("toString", "Generate toString"); + } + + createEqualityFix() { + return this.constructQuickFix("equality", "Generate equality"); + } + + createUseEquatableFix() { + return this.constructQuickFix("useEquatable", `Generate Equatable`); + } + + createImportsFix() { + const imports = new Imports(this.document.getText()); + if (!imports.didChange) return; + + const inImportsRange = + this.lineNumber >= imports.startAtLine && + this.lineNumber <= imports.endAtLine; + if (inImportsRange) { + let title = "Sort imports"; + if (imports.hasImportDeclaration && imports.hasExportDeclaration) { + title = "Sort imports/exports"; + } else if (imports.hasExportDeclaration) { + title = "Sort exports"; + } + + return this.createFix(title, (edit) => { + edit.replace(this.uri, imports.range, imports.formatted); + }); + } + } + + getClass() { + for (let clazz of this.generator.clazzes) { + if ( + clazz.startsAtLine <= this.lineNumber && + clazz.endsAtLine >= this.lineNumber + ) { + return clazz; + } + } + } } /** @@ -2386,95 +2630,107 @@ class DataClassCodeActions { * @param {Imports} imports */ function getReplaceEdit(values, imports = null, showLogs = false) { - /** @type {DartClass[]} */ - const clazzes = values instanceof DartClass ? [values] : values; - const hasMultiple = clazzes.length > 1; - const edit = new vscode.WorkspaceEdit(); - const uri = getDoc().uri; - - const noChanges = []; - for (var i = clazzes.length - 1; i >= 0; i--) { - const clazz = clazzes[i]; - - if (clazz.isValid) { - if (clazz.didChange) { - let replacement = clazz.generateClassReplacement(); - // separate the classes with a new line when multiple - // classes are being generated. - if (!clazz.isLastInFile) { - replacement += '\n'; - } - - if (!isBlank(replacement)) { - edit.replace(uri, new vscode.Range( - new vscode.Position((clazz.startsAtLine - 1), 0), - new vscode.Position(clazz.endsAtLine, 1) - ), replacement); - } - } else if (showLogs) { - noChanges.push(clazz.name); - if (i == 0) { - const info = noChanges.length == 1 ? `class ${noChanges[0]}` : `classes ${noChanges.join(', ')}`; - showInfo(`No changes detected for ${info}`); - } - } - } else if (showLogs) { - showError(clazz.issue); - } - } - - // If imports need to be inserted, do it at the top of the file. - if (imports != null && imports.hasImports) { - // Imports must be separated by at least one line because otherwise we get an overlapping range error - // from the vscode editor. - const areImportsseparated = !hasMultiple || (imports.startAtLine || 0) < clazzes[0].startsAtLine - 1; - if (imports.hasPreviousImports && areImportsseparated) { - edit.replace(uri, imports.range, imports.formatted); - } else { - edit.insert(uri, new vscode.Position(imports.startAtLine, 0), imports.formatted + '\n'); - } + /** @type {DartClass[]} */ + const clazzes = values instanceof DartClass ? [values] : values; + const hasMultiple = clazzes.length > 1; + const edit = new vscode.WorkspaceEdit(); + const uri = getDoc().uri; + + const noChanges = []; + for (var i = clazzes.length - 1; i >= 0; i--) { + const clazz = clazzes[i]; + + if (clazz.isValid) { + if (clazz.didChange) { + let replacement = clazz.generateClassReplacement(); + // separate the classes with a new line when multiple + // classes are being generated. + if (!clazz.isLastInFile) { + replacement += "\n"; + } + + if (!isBlank(replacement)) { + edit.replace( + uri, + new vscode.Range( + new vscode.Position(clazz.startsAtLine - 1, 0), + new vscode.Position(clazz.endsAtLine, 1) + ), + replacement + ); + } + } else if (showLogs) { + noChanges.push(clazz.name); + if (i == 0) { + const info = + noChanges.length == 1 + ? `class ${noChanges[0]}` + : `classes ${noChanges.join(", ")}`; + showInfo(`No changes detected for ${info}`); + } + } + } else if (showLogs) { + showError(clazz.issue); + } + } + + // If imports need to be inserted, do it at the top of the file. + if (imports != null && imports.hasImports) { + // Imports must be separated by at least one line because otherwise we get an overlapping range error + // from the vscode editor. + const areImportsseparated = + !hasMultiple || (imports.startAtLine || 0) < clazzes[0].startsAtLine - 1; + if (imports.hasPreviousImports && areImportsseparated) { + edit.replace(uri, imports.range, imports.formatted); + } else { + edit.insert( + uri, + new vscode.Position(imports.startAtLine, 0), + imports.formatted + "\n" + ); } + } - return edit; + return edit; } /** * @param {string} str */ function isBlank(str) { - return (!str || /^\s*$/.test(str)); + return !str || /^\s*$/.test(str); } /** * @param {string} name */ function createFileName(name) { - let r = ''; - for (let i = 0; i < name.length; i++) { - let c = name[i]; - if (c == c.toUpperCase()) { - if (i == 0) r += c.toLowerCase(); - else r += '_' + c.toLowerCase(); - } else { - r += c; - } + let r = ""; + for (let i = 0; i < name.length; i++) { + let c = name[i]; + if (c == c.toUpperCase()) { + if (i == 0) r += c.toLowerCase(); + else r += "_" + c.toLowerCase(); + } else { + r += c; } + } - return r; + return r; } function getCurrentPath() { - let path = vscode.window.activeTextEditor.document.fileName; - let dirs = path.split("\\"); - path = ''; - for (let i = 0; i < dirs.length; i++) { - let dir = dirs[i]; - if (i < dirs.length - 1) { - path += dir + "\\"; - } + let path = vscode.window.activeTextEditor.document.fileName; + let dirs = path.split("\\"); + path = ""; + for (let i = 0; i < dirs.length; i++) { + let dir = dirs[i]; + if (i < dirs.length - 1) { + path += dir + "\\"; } + } - return path; + return path; } /** @@ -2482,21 +2738,21 @@ function getCurrentPath() { * @param {string} name */ async function writeFile(content, name, open = true, path = getCurrentPath()) { - let p = path + name + '.dart'; - if (fs.existsSync(p)) { - let i = 0; - do { - p = path + name + '_' + ++i + '.dart' - } while (fs.existsSync(p)); - } - - fs.writeFileSync(p, content, 'utf8'); - if (open) { - let openPath = vscode.Uri.parse("file:///" + p); - let doc = await vscode.workspace.openTextDocument(openPath); - await vscode.window.showTextDocument(doc); - } - return; + let p = path + name + ".dart"; + if (fs.existsSync(p)) { + let i = 0; + do { + p = path + name + "_" + ++i + ".dart"; + } while (fs.existsSync(p)); + } + + fs.writeFileSync(p, content, "utf8"); + if (open) { + let openPath = vscode.Uri.parse("file:///" + p); + let doc = await vscode.workspace.openTextDocument(openPath); + await vscode.window.showTextDocument(doc); + } + return; } /** @@ -2504,81 +2760,107 @@ async function writeFile(content, name, open = true, path = getCurrentPath()) { * @param {string} source */ function toVarName(source) { - let s = source; - let r = ''; - - /** - * @param {string} char - */ - let replace = (char) => { - if (s.includes(char)) { - const splits = s.split(char); - for (let i = 0; i < splits.length; i++) { - let w = splits[i]; - i > 0 ? r += capitalize(w) : r += w; - } - } - } - - // Replace invalid variable characters like '-'. - replace('-'); - replace('~'); - replace(':'); - replace('#'); - replace('$'); - - if (r.length == 0) - r = s; - - // Prevent dart keywords from being used. - const keywords = [ - 'assert', 'break', 'case', 'catch', 'class', 'const', 'continue', - 'default', 'do', 'else', 'enum', 'extends', 'false', 'final', - 'finally', 'for', 'if', 'in', 'is', 'new', 'null', 'rethrow', - 'return', 'super', 'switch', 'this', 'throw', 'true', 'try', - 'var', 'void', 'while', 'with' - ]; - - if (keywords.includes(r)) { - r = r + '_'; - } - - if (r.length > 0 && r[0].match(new RegExp(/[0-9]/))) - r = 'n' + r; - - return r; + let s = source; + let r = ""; + + /** + * @param {string} char + */ + let replace = (char) => { + if (s.includes(char)) { + const splits = s.split(char); + for (let i = 0; i < splits.length; i++) { + let w = splits[i]; + i > 0 ? (r += capitalize(w)) : (r += w); + } + } + }; + + // Replace invalid variable characters like '-'. + replace("-"); + replace("~"); + replace(":"); + replace("#"); + replace("$"); + + if (r.length == 0) r = s; + + // Prevent dart keywords from being used. + const keywords = [ + "assert", + "break", + "case", + "catch", + "class", + "const", + "continue", + "default", + "do", + "else", + "enum", + "extends", + "false", + "final", + "finally", + "for", + "if", + "in", + "is", + "new", + "null", + "rethrow", + "return", + "super", + "switch", + "this", + "throw", + "true", + "try", + "var", + "void", + "while", + "with", + ]; + + if (keywords.includes(r)) { + r = r + "_"; + } + + if (r.length > 0 && r[0].match(new RegExp(/[0-9]/))) r = "n" + r; + + return r; } function camelCase(str) { - const snakeToCamel = - str.replace(/([-_][a-z])/g, group => - group - .toUpperCase() - .replace('-', '') - .replace('_', '') - ); + const snakeToCamel = str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace("-", "").replace("_", "") + ); - return snakeToCamel; + return snakeToCamel; } /** * @param {string} src */ function varToKey(src) { - const snakeCase = string => { - return string.replace(/\W+/g, " ") - .split(/ |\B(?=[A-Z])/) - .map(word => word.toLowerCase()) - .join('_'); - }; - - const format = readSetting("json.key_format") - - switch (format) { - case 'snake_case': return snakeCase(src); - case 'camelCase': return camelCase(src); - default: return src; - } + const snakeCase = (string) => { + return string + .replace(/\W+/g, " ") + .split(/ |\B(?=[A-Z])/) + .map((word) => word.toLowerCase()) + .join("_"); + }; + + const format = readSetting("json.key_format"); + + switch (format) { + case "snake_case": + return snakeCase(src); + case "camelCase": + return camelCase(src); + default: + return src; + } } /** @@ -2588,12 +2870,13 @@ function varToKey(src) { * @param {string} value */ function editorReplace(editor, start = null, end = null, value) { - editor.replace(new vscode.Range( - new vscode.Position(start || 0, 0), - new vscode.Position(end || getDocText().split('\n').length, 1) + editor.replace( + new vscode.Range( + new vscode.Position(start || 0, 0), + new vscode.Position(end || getDocText().split("\n").length, 1) ), - value - ); + value + ); } /** @@ -2602,7 +2885,7 @@ function editorReplace(editor, start = null, end = null, value) { * @param {string} value */ function editorInsert(editor, at, value) { - editor.insert(new vscode.Position(at, 0), value); + editor.insert(new vscode.Position(at, 0), value); } /** @@ -2611,12 +2894,12 @@ function editorInsert(editor, at, value) { * @param {number} to */ function editorDelete(editor, from = null, to = null) { - editor.delete( - new vscode.Range( - new vscode.Position(from || 0, 0), - new vscode.Position(to || getDocText().split('\n').length, 1) - ) - ); + editor.delete( + new vscode.Range( + new vscode.Position(from || 0, 0), + new vscode.Position(to || getDocText().split("\n").length, 1) + ) + ); } /** @@ -2624,33 +2907,36 @@ function editorDelete(editor, from = null, to = null) { * @param {number} to */ function scrollTo(from = null, to = null) { - getEditor().revealRange( - new vscode.Range( - new vscode.Position(from || 0, 0), - new vscode.Position(to || 0, 0) - ), - 0 - ); + getEditor().revealRange( + new vscode.Range( + new vscode.Position(from || 0, 0), + new vscode.Position(to || 0, 0) + ), + 0 + ); } function clearSelection() { - getEditor().selection = new vscode.Selection(new vscode.Position(0, 0), new vscode.Position(0, 0)); + getEditor().selection = new vscode.Selection( + new vscode.Position(0, 0), + new vscode.Position(0, 0) + ); } /** * @param {string} source */ function capitalize(source) { - let s = source; - if (s.length > 0) { - if (s.length > 1) { - return s.substr(0, 1).toUpperCase() + s.substring(1, s.length); - } else { - return s.substr(0, 1).toUpperCase(); - } + let s = source; + if (s.length > 0) { + if (s.length > 1) { + return s.substr(0, 1).toUpperCase() + s.substring(1, s.length); + } else { + return s.substr(0, 1).toUpperCase(); } + } - return s; + return s; } /** @@ -2658,15 +2944,17 @@ function capitalize(source) { * @param {string | any[]} start */ function removeStart(source, start) { - if (Array.isArray(start)) { - let result = source.trim(); - for (let s of start) { - result = removeStart(result, s).trim(); - } - return result; - } else { - return source.startsWith(start) ? source.substring(start.length, source.length) : source; + if (Array.isArray(start)) { + let result = source.trim(); + for (let s of start) { + result = removeStart(result, s).trim(); } + return result; + } else { + return source.startsWith(start) + ? source.substring(start.length, source.length) + : source; + } } /** @@ -2674,44 +2962,44 @@ function removeStart(source, start) { * @param {string | any[]} end */ function removeEnd(source, end) { - if (Array.isArray(end)) { - let result = source.trim(); - for (let e of end) { - result = removeEnd(result, e).trim(); - } - return result; - } else { - const pos = (source.length - end.length); - return source.endsWith(end) ? source.substring(0, pos) : source; + if (Array.isArray(end)) { + let result = source.trim(); + for (let e of end) { + result = removeEnd(result, e).trim(); } + return result; + } else { + const pos = source.length - end.length; + return source.endsWith(end) ? source.substring(0, pos) : source; + } } /** * @param {string} source */ function indent(source) { - let r = ''; - for (let line of source.split('\n')) { - r += ' ' + line + '\n'; - } - return r.length > 0 ? r : source; + let r = ""; + for (let line of source.split("\n")) { + r += " " + line + "\n"; + } + return r.length > 0 ? r : source; } /** -* @param {string} source -* @param {string} match -*/ + * @param {string} source + * @param {string} match + */ function count(source, match) { - let count = 0; - let length = match.length; - for (let i = 0; i < source.length; i++) { - let part = source.substr((i * length) - 1, length); - if (part == match) { - count++; - } + let count = 0; + let length = match.length; + for (let i = 0; i < source.length; i++) { + let part = source.substr(i * length - 1, length); + if (part == match) { + count++; } + } - return count; + return count; } /** @@ -2719,183 +3007,183 @@ function count(source, match) { * @param {string} b */ function areStrictEqual(a, b) { - let x = a.replace(/\s/g, ""); - let y = b.replace(/\s/g, ""); - return x === y; + let x = a.replace(/\s/g, ""); + let y = b.replace(/\s/g, ""); + return x === y; } /** -* @param {string} source -* @param {string[]} matches -*/ + * @param {string} source + * @param {string[]} matches + */ function removeAll(source, matches) { - let r = ''; - for (let s of source) { - if (!matches.includes(s)) { - r += s; - } + let r = ""; + for (let s of source) { + if (!matches.includes(s)) { + r += s; } - return r; + } + return r; } /** -* @param {string} source -* @param {string[]} matches -*/ + * @param {string} source + * @param {string[]} matches + */ function includesOne(source, matches, wordBased = true) { - const words = wordBased ? source.split(' ') : [source]; - for (let word of words) { - for (let match of matches) { - if (wordBased) { - if (word === match) - return true; - } else { - if (source.includes(match)) - return true; - } - } + const words = wordBased ? source.split(" ") : [source]; + for (let word of words) { + for (let match of matches) { + if (wordBased) { + if (word === match) return true; + } else { + if (source.includes(match)) return true; + } } + } - return false; + return false; } /** -* @param {string} source -* @param {string[]} matches -*/ + * @param {string} source + * @param {string[]} matches + */ function includesAll(source, matches) { - for (let match of matches) { - if (!source.includes(match)) - return false; - } - return true; + for (let match of matches) { + if (!source.includes(match)) return false; + } + return true; } function getEditor() { - return vscode.window.activeTextEditor; + return vscode.window.activeTextEditor; } function getDoc() { - return getEditor().document; + return getEditor().document; } function getDocText() { - return getDoc().getText(); + return getDoc().getText(); } function getLangId() { - return getDoc().languageId; + return getDoc().languageId; } /** * @param {string} key */ function readSetting(key) { - return vscode.workspace.getConfiguration().get('dart-data-class-generator.' + key); + return vscode.workspace + .getConfiguration() + .get("dart-data-class-generator." + key); } /** * @param {string} typeName */ function readCustomTypeSetting(typeName) { - const customTypes = readSetting('custom.types'); - const customTypeConfig = customTypes.find(custom => custom.type === typeName); - - // { - // "type": "DateTime", - // "fromMap": "DateTime.parse(String)", - // "toMap": "toIso8601String()" - // } - return customTypeConfig; + const customTypes = readSetting("custom.types"); + const customTypeConfig = customTypes.find( + (custom) => custom.type === typeName + ); + + // { + // "type": "DateTime", + // "fromMap": "DateTime.parse(String)", + // "toMap": "toIso8601String()" + // } + return customTypeConfig; } /** * @param {string} fromMap */ function extractFromMap(fromMap) { - // fromCustom(type ?? def) || fromCustom[type ?? def] - const regex = /(\w+)([\[(])((?:[^()\[\]]|\((?:[^()]*\)))*)((?:[\])]))/; - - const r = regex.exec(fromMap); + // fromCustom(type ?? def) || fromCustom[type ?? def] + const regex = /(\w+)([\[(])((?:[^()\[\]]|\((?:[^()]*\)))*)((?:[\])]))/; - // [ 'fromCustom(int ?? 0)' , 'fromCustom' , '(' , 'type ?? def' , ')' ] - if (r) { - const [from, open, typedef, close] = r.slice(1); + const r = regex.exec(fromMap); - let fixFrom = from; - - if (!fromMap.includes('.')) { - fixFrom = ''; - } + // [ 'fromCustom(int ?? 0)' , 'fromCustom' , '(' , 'type ?? def' , ')' ] + if (r) { + const [from, open, typedef, close] = r.slice(1); - return [fixFrom, open, typedef, close]; + let fixFrom = from; + if (!fromMap.includes(".")) { + fixFrom = ""; } - return ['', '', '', '']; + + return [fixFrom, open, typedef, close]; + } + return ["", "", "", ""]; } /** * @param {string[]} keys */ function readSettings(keys) { - for (let key of keys) { - if (readSetting(key)) { - return true; - } + for (let key of keys) { + if (readSetting(key)) { + return true; } + } - return false; + return false; } /** * @param {string} msg */ function showError(msg) { - vscode.window.showErrorMessage(msg); + vscode.window.showErrorMessage(msg); } /** * @param {string} msg */ function showInfo(msg) { - vscode.window.showInformationMessage(msg); + vscode.window.showInformationMessage(msg); } -function deactivate() { } +function deactivate() {} module.exports = { - activate, - deactivate, - generateDataClass, - generateJsonDataClass, - DartFile, - DartClass, - DataClassGenerator, - JsonReader, - ClassPart, - ClassField, - writeFile, - getCurrentPath, - toVarName, - createFileName, - editorInsert, - editorReplace, - editorDelete, - capitalize, - scrollTo, - clearSelection, - removeEnd, - indent, - count, - areStrictEqual, - removeAll, - includesOne, - includesAll, - getEditor, - getDoc, - getDocText, - getLangId, - readSetting, - showError, - showInfo -} + activate, + deactivate, + generateDataClass, + generateJsonDataClass, + DartFile, + DartClass, + DataClassGenerator, + JsonReader, + ClassPart, + ClassField, + writeFile, + getCurrentPath, + toVarName, + createFileName, + editorInsert, + editorReplace, + editorDelete, + capitalize, + scrollTo, + clearSelection, + removeEnd, + indent, + count, + areStrictEqual, + removeAll, + includesOne, + includesAll, + getEditor, + getDoc, + getDocText, + getLangId, + readSetting, + showError, + showInfo, +};