From 86a8d8410aa95e2593d1b16b224b900727bf1ae7 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:03:14 +0200 Subject: [PATCH 01/16] Add FVM Support --- .fvmrc | 3 +++ .gitignore | 5 +++-- .vscode/settings.json | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .fvmrc create mode 100644 .vscode/settings.json diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..4cac08f7 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.29.2" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 33f634c5..8e13de86 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,6 @@ app.*.map.json /android/app/release pubspec.lock -.fvm/flutter_sdk -.fvm/fvm_config.json + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..7ce2e0e0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/3.29.2" +} \ No newline at end of file From 2440cf2aa0baddae95506cfb7bafdf321b46f039 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:14:23 +0200 Subject: [PATCH 02/16] Revert "Add FVM Support" This reverts commit 86a8d8410aa95e2593d1b16b224b900727bf1ae7. --- .fvmrc | 3 --- .gitignore | 5 ++--- .vscode/settings.json | 3 --- 3 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 .fvmrc delete mode 100644 .vscode/settings.json diff --git a/.fvmrc b/.fvmrc deleted file mode 100644 index 4cac08f7..00000000 --- a/.fvmrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "flutter": "3.29.2" -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8e13de86..33f634c5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,5 @@ app.*.map.json /android/app/release pubspec.lock - -# FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/flutter_sdk +.fvm/fvm_config.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7ce2e0e0..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dart.flutterSdkPath": ".fvm/versions/3.29.2" -} \ No newline at end of file From 9dff5fde4ed12b6c65b3b5acf081e898275e8534 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:21:54 +0200 Subject: [PATCH 03/16] Fix Scroll Controller Null Check issue --- lib/src/code_field/code_field.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 03a3d876..407053be 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -331,7 +331,7 @@ class _CodeFieldState extends State { if (line.length > longestLine.length) longestLine = line; }); - if (_codeScroll != null && _editorKey.currentContext != null) { + if (_codeScroll != null && _codeScroll!.hasClients && _editorKey.currentContext != null) { final box = _editorKey.currentContext!.findRenderObject() as RenderBox?; _editorOffset = box?.localToGlobal(Offset.zero); if (_editorOffset != null) { From 700cfa8644a536fa71c979c0091195e5637598c8 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:37:26 +0200 Subject: [PATCH 04/16] Code cleanup --- lib/src/code_field/code_field.dart | 56 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 407053be..e9449447 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -206,11 +206,15 @@ class CodeField extends StatefulWidget { class _CodeFieldState extends State { // Add a controller - LinkedScrollControllerGroup? _controllers; - ScrollController? _numberScroll; - ScrollController? _codeScroll; - ScrollController? _horizontalCodeScroll; - final _codeFieldKey = GlobalKey(); + final LinkedScrollControllerGroup _controllers = + LinkedScrollControllerGroup(); + + late final ScrollController _numberScroll; + late final ScrollController _codeScroll; + late final ScrollController _horizontalCodeScroll; + + final GlobalKey> _codeFieldKey = + GlobalKey>(); OverlayEntry? _suggestionsPopup; OverlayEntry? _searchPopup; @@ -226,15 +230,16 @@ class _CodeFieldState extends State { late TextStyle textStyle; Color? _backgroundCol; - final _editorKey = GlobalKey(); + final GlobalKey> _editorKey = + GlobalKey>(); Offset? _editorOffset; @override void initState() { super.initState(); - _controllers = LinkedScrollControllerGroup(); - _numberScroll = _controllers?.addAndGet(); - _codeScroll = _controllers?.addAndGet(); + + _numberScroll = _controllers.addAndGet(); + _codeScroll = _controllers.addAndGet(); widget.controller.addListener(_onTextChanged); widget.controller.addListener(_updatePopupOffset); @@ -276,9 +281,9 @@ class _CodeFieldState extends State { ); _searchPopup?.remove(); _searchPopup = null; - _numberScroll?.dispose(); - _codeScroll?.dispose(); - _horizontalCodeScroll?.dispose(); + _numberScroll.dispose(); + _codeScroll.dispose(); + _horizontalCodeScroll.dispose(); super.dispose(); } @@ -331,12 +336,13 @@ class _CodeFieldState extends State { if (line.length > longestLine.length) longestLine = line; }); - if (_codeScroll != null && _codeScroll!.hasClients && _editorKey.currentContext != null) { - final box = _editorKey.currentContext!.findRenderObject() as RenderBox?; + if (_editorKey.currentContext != null) { + final RenderBox? box = + _editorKey.currentContext!.findRenderObject() as RenderBox?; _editorOffset = box?.localToGlobal(Offset.zero); if (_editorOffset != null) { - var fixedOffset = _editorOffset!; - fixedOffset += Offset(0, _codeScroll!.offset); + Offset fixedOffset = _editorOffset!; + fixedOffset += Offset(0, _codeScroll.offset); _editorOffset = fixedOffset; } } @@ -354,7 +360,7 @@ class _CodeFieldState extends State { child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + children: [ ConstrainedBox( constraints: BoxConstraints( maxHeight: 0, @@ -383,10 +389,10 @@ class _CodeFieldState extends State { @override Widget build(BuildContext context) { // Default color scheme - const rootKey = 'root'; + const String rootKey = 'root'; - final themeData = Theme.of(context); - final styles = CodeTheme.of(context)?.styles; + final ThemeData themeData = Theme.of(context); + final Map? styles = CodeTheme.of(context)?.styles; _backgroundCol = widget.background ?? styles?[rootKey]?.backgroundColor ?? DefaultStyles.backgroundColor; @@ -402,7 +408,7 @@ class _CodeFieldState extends State { textStyle = defaultTextStyle.merge(widget.textStyle); - final codeField = TextField( + final TextField codeField = TextField( focusNode: _focusNode, scrollPadding: widget.padding, style: textStyle, @@ -516,18 +522,18 @@ class _CodeFieldState extends State { } double _getCaretHeight(TextPainter textPainter) { - final double? caretFullHeight = textPainter.getFullHeightForCaret( + final double caretFullHeight = textPainter.getFullHeightForCaret( widget.controller.selection.base, Rect.zero, ); - return caretFullHeight ?? 0; + return caretFullHeight; } double _getPopupLeftOffset(TextPainter textPainter) { return max( _getCaretOffset(textPainter).dx + widget.padding.left - - _horizontalCodeScroll!.offset + + _horizontalCodeScroll.offset + (_editorOffset?.dx ?? 0), 0, ); @@ -539,7 +545,7 @@ class _CodeFieldState extends State { caretHeight + 16 + widget.padding.top - - _codeScroll!.offset + + _codeScroll.offset + (_editorOffset?.dy ?? 0), 0, ); From 564ccb5d0b89f10643dabe0547384e66025f0502 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:42:00 +0200 Subject: [PATCH 05/16] fix the scroll controller issue properly --- lib/src/code_field/code_field.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index e9449447..6bdf4c9a 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -258,11 +258,11 @@ class _CodeFieldState extends State { disableSpellCheckIfWeb(); WidgetsBinding.instance.addPostFrameCallback((_) { + _onTextChanged(); final double width = _codeFieldKey.currentContext!.size!.width; final double height = _codeFieldKey.currentContext!.size!.height; windowSize = Size(width, height); }); - _onTextChanged(); } KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { @@ -342,7 +342,9 @@ class _CodeFieldState extends State { _editorOffset = box?.localToGlobal(Offset.zero); if (_editorOffset != null) { Offset fixedOffset = _editorOffset!; - fixedOffset += Offset(0, _codeScroll.offset); + if (_codeScroll.hasClients) { + fixedOffset += Offset(0, _codeScroll.offset); + } _editorOffset = fixedOffset; } } From 7da907d4347bd1cc2bfd59a2e675bca5d8535cac Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:45:16 +0200 Subject: [PATCH 06/16] fix state management + scroll offset if the scroll controller is not set --- lib/src/code_field/code_field.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 6bdf4c9a..c9a425b7 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -503,10 +503,12 @@ class _CodeFieldState extends State { final flippedTopOffset = normalTopOffset - (Sizes.autocompletePopupMaxHeight + caretHeight + Sizes.caretPadding); - setState(() { - _normalPopupOffset = Offset(leftOffset, normalTopOffset); - _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); - }); + if (mounted) { + setState(() { + _normalPopupOffset = Offset(leftOffset, normalTopOffset); + _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); + }); + } } TextPainter _getTextPainter(String text) { @@ -532,10 +534,13 @@ class _CodeFieldState extends State { } double _getPopupLeftOffset(TextPainter textPainter) { + final double scrollOffset = + _horizontalCodeScroll.hasClients ? _horizontalCodeScroll.offset : 0; + return max( _getCaretOffset(textPainter).dx + widget.padding.left - - _horizontalCodeScroll.offset + + scrollOffset + (_editorOffset?.dx ?? 0), 0, ); From 4569a6b334293fb65d1381776cc4ed4a101aaca6 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:49:57 +0200 Subject: [PATCH 07/16] fix more of state management --- lib/src/code_field/code_field.dart | 36 +++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index c9a425b7..609318aa 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -307,18 +307,20 @@ class _CodeFieldState extends State { } void rebuild() { - setState(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - // For some reason _codeFieldKey.currentContext is null in tests - // so check first. - final context = _codeFieldKey.currentContext; - if (context != null) { - final double width = context.size!.width; - final double height = context.size!.height; - windowSize = Size(width, height); - } + if (mounted) { + setState(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + // For some reason _codeFieldKey.currentContext is null in tests + // so check first. + final context = _codeFieldKey.currentContext; + if (context != null) { + final double width = context.size!.width; + final double height = context.size!.height; + windowSize = Size(width, height); + } + }); }); - }); + } } void _onTextChanged() { @@ -503,11 +505,11 @@ class _CodeFieldState extends State { final flippedTopOffset = normalTopOffset - (Sizes.autocompletePopupMaxHeight + caretHeight + Sizes.caretPadding); + _normalPopupOffset = Offset(leftOffset, normalTopOffset); + _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); + if (mounted) { - setState(() { - _normalPopupOffset = Offset(leftOffset, normalTopOffset); - _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); - }); + setState(() {}); } } @@ -547,12 +549,14 @@ class _CodeFieldState extends State { } double _getPopupTopOffset(TextPainter textPainter, double caretHeight) { + final codeScrollOffset = _codeScroll.hasClients ? _codeScroll.offset : 0; + return max( _getCaretOffset(textPainter).dy + caretHeight + 16 + widget.padding.top - - _codeScroll.offset + + codeScrollOffset + (_editorOffset?.dy ?? 0), 0, ); From b3150416a0061dafc77b7fdf17d6364eef1c2abf Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:52:47 +0200 Subject: [PATCH 08/16] The currentContext.size is null when it first runs --- lib/src/code_field/code_field.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 609318aa..ef7a3187 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -312,12 +312,12 @@ class _CodeFieldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { // For some reason _codeFieldKey.currentContext is null in tests // so check first. - final context = _codeFieldKey.currentContext; - if (context != null) { - final double width = context.size!.width; - final double height = context.size!.height; - windowSize = Size(width, height); - } + final BuildContext? context = _codeFieldKey.currentContext; + if (context == null || context.size == null) return; + + final double width = context.size!.width; + final double height = context.size!.height; + windowSize = Size(width, height); }); }); } From 693c652d49b60c87de932ee21b9bdbaef3ba8a89 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 22:57:14 +0200 Subject: [PATCH 09/16] Unnecessary code call -> Already defined in rebuild() --- lib/src/code_field/code_field.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index ef7a3187..d01a2bce 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -259,9 +259,6 @@ class _CodeFieldState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { _onTextChanged(); - final double width = _codeFieldKey.currentContext!.size!.width; - final double height = _codeFieldKey.currentContext!.size!.height; - windowSize = Size(width, height); }); } From 0b0129cfaed7f5ae469a2b829cba89beea3d8bbf Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 22 Mar 2025 23:02:30 +0200 Subject: [PATCH 10/16] Fix gutter error widget state management --- lib/src/gutter/error.dart | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/src/gutter/error.dart b/lib/src/gutter/error.dart index 04abf601..0a9b529f 100644 --- a/lib/src/gutter/error.dart +++ b/lib/src/gutter/error.dart @@ -33,15 +33,17 @@ class _GutterErrorWidgetState extends State { Widget build(BuildContext context) { return MouseRegion( onEnter: (event) { - setState(() { - _mouseEnteredPopup = false; - if (_entry != null) { - return; - } - _entry = _getErrorPopup(); - final overlay = Overlay.of(context); - overlay.insert(_entry!); - }); + if (mounted) { + setState(() { + _mouseEnteredPopup = false; + if (_entry != null) { + return; + } + _entry = _getErrorPopup(); + final overlay = Overlay.of(context); + overlay.insert(_entry!); + }); + } }, onExit: (event) { // Delay event here to keep overlay @@ -49,12 +51,14 @@ class _GutterErrorWidgetState extends State { Future.delayed( const Duration(milliseconds: 50), () { - setState(() { - if (!_mouseEnteredPopup) { - _entry?.remove(); - _entry = null; - } - }); + if (mounted) { + setState(() { + if (!_mouseEnteredPopup) { + _entry?.remove(); + _entry = null; + } + }); + } }, ); }, From 1fdf9a4944ab5dfb2894e33b0657cca8d7a1509b Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:37:38 +0200 Subject: [PATCH 11/16] Add more tests + test packages + bug fixes --- build.yaml | 11 + lib/src/analyzer/dartpad_analyzer.dart | 36 +- lib/src/analyzer/models/issue.dart | 2 +- lib/src/code_field/code_field.dart | 10 +- pubspec.yaml | 2 + test/mocks/general_mocks.dart | 14 + test/mocks/general_mocks.mocks.dart | 368 ++++++++++++++++++ test/src/analyzer/abstract_analyzer_test.dart | 32 ++ test/src/analyzer/dartpad_analyzer_test.dart | 76 ++++ .../analyzer/default_local_analyzer_test.dart | 63 +++ .../analyzer/models/analysis_result_test.dart | 21 + test/src/analyzer/models/issue_test.dart | 73 ++++ 12 files changed, 685 insertions(+), 23 deletions(-) create mode 100644 build.yaml create mode 100644 test/mocks/general_mocks.dart create mode 100644 test/mocks/general_mocks.mocks.dart create mode 100644 test/src/analyzer/abstract_analyzer_test.dart create mode 100644 test/src/analyzer/dartpad_analyzer_test.dart create mode 100644 test/src/analyzer/default_local_analyzer_test.dart create mode 100644 test/src/analyzer/models/analysis_result_test.dart create mode 100644 test/src/analyzer/models/issue_test.dart diff --git a/build.yaml b/build.yaml new file mode 100644 index 00000000..5aaf9be8 --- /dev/null +++ b/build.yaml @@ -0,0 +1,11 @@ +targets: + $default: + sources: + - $package$ + - lib/$lib$ + - lib/**.dart + - test/**.dart + builders: + mockito|mockBuilder: + generate_for: + - test/**.dart diff --git a/lib/src/analyzer/dartpad_analyzer.dart b/lib/src/analyzer/dartpad_analyzer.dart index e33ad882..62e54852 100644 --- a/lib/src/analyzer/dartpad_analyzer.dart +++ b/lib/src/analyzer/dartpad_analyzer.dart @@ -1,4 +1,3 @@ -// ignore_for_file: avoid_dynamic_calls import 'dart:convert'; import 'package:http/http.dart' as http; @@ -11,41 +10,44 @@ import 'models/issue_type.dart'; // Example for implementation of Analyzer for Dart. class DartPadAnalyzer extends AbstractAnalyzer { - static const _url = + static const String url = 'https://stable.api.dartpad.dev/api/dartservices/v2/analyze'; + final http.Client client; + + DartPadAnalyzer({http.Client? client}) : client = client ?? http.Client(); + @override Future analyze(Code code) async { - final client = http.Client(); - - final response = await client.post( - Uri.parse(_url), - body: json.encode({ - 'source': code.text, - }), + final http.Response response = await client.post( + Uri.parse(url), + body: json.encode({'source': code.text}), encoding: utf8, ); - final decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map; - final issueMaps = decodedResponse['issues']; + final Map decodedResponse = + jsonDecode(utf8.decode(response.bodyBytes)) as Map; + final dynamic issueMaps = decodedResponse['issues']; - if (issueMaps is! Iterable || (issueMaps.isEmpty)) { - return const AnalysisResult(issues: []); + if (issueMaps is! Iterable || (issueMaps.isEmpty)) { + return const AnalysisResult(issues: []); } - final issues = issueMaps + final List issues = issueMaps .cast>() - .map(issueFromJson) + .map(issueFromJson) .toList(growable: false); + return AnalysisResult(issues: issues); } } // Converts json to Issue object for the DartAnalyzer. Issue issueFromJson(Map json) { - final type = mapIssueType(json['kind']); + final IssueType type = mapIssueType(json['kind']); + final int line = json['line']; return Issue( - line: json['line'] - 1, + line: line - 1, message: json['message'], suggestion: json['correction'], type: type, diff --git a/lib/src/analyzer/models/issue.dart b/lib/src/analyzer/models/issue.dart index 8d90e6ad..60a103ae 100644 --- a/lib/src/analyzer/models/issue.dart +++ b/lib/src/analyzer/models/issue.dart @@ -18,6 +18,6 @@ class Issue { }); } -Comparator issueLineComparator = (issue1, issue2) { +Comparator issueLineComparator = (Issue issue1, Issue issue2) { return issue1.line - issue2.line; }; diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 3625df2f..9a249523 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -512,11 +512,11 @@ class _CodeFieldState extends State { final flippedTopOffset = normalTopOffset - (Sizes.autocompletePopupMaxHeight + caretHeight + Sizes.caretPadding); - _normalPopupOffset = Offset(leftOffset, normalTopOffset); - _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); - if (mounted) { - setState(() {}); + setState(() { + _normalPopupOffset = Offset(leftOffset, normalTopOffset); + _flippedPopupOffset = Offset(leftOffset, flippedTopOffset); + }); } } @@ -603,7 +603,7 @@ class _CodeFieldState extends State { OverlayEntry _buildSearchOverlay() { final colorScheme = Theme.of(context).colorScheme; - final borderColor = _getTextColorFromTheme() ?? colorScheme.onBackground; + final borderColor = _getTextColorFromTheme() ?? colorScheme.onSurface; return OverlayEntry( builder: (context) { return Positioned( diff --git a/pubspec.yaml b/pubspec.yaml index 0cd5720b..2df65a73 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,8 +25,10 @@ dependencies: url_launcher: ^6.1.8 dev_dependencies: + build_runner: ^2.4.15 fake_async: ^1.3.1 flutter_test: { sdk: flutter } + mockito: ^5.4.5 total_lints: ^3.1.1 flutter: diff --git a/test/mocks/general_mocks.dart b/test/mocks/general_mocks.dart new file mode 100644 index 00000000..b4474175 --- /dev/null +++ b/test/mocks/general_mocks.dart @@ -0,0 +1,14 @@ +import 'package:http/http.dart' as http; +import 'package:mockito/annotations.dart' show GenerateNiceMocks, MockSpec; +/* +Run `dart run build_runner watch` in the +command line to generate the mocks +*/ + +// Create mock classes +@GenerateNiceMocks( + >[ + MockSpec(), + ], +) +void main() {} diff --git a/test/mocks/general_mocks.mocks.dart b/test/mocks/general_mocks.mocks.dart new file mode 100644 index 00000000..2c8d70f0 --- /dev/null +++ b/test/mocks/general_mocks.mocks.dart @@ -0,0 +1,368 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in flutter_code_editor/test/mocks/general_mocks.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; +import 'dart:convert' as _i4; +import 'dart:typed_data' as _i6; + +import 'package:http/http.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i5; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeResponse_0 extends _i1.SmartFake implements _i2.Response { + _FakeResponse_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeStreamedResponse_1 extends _i1.SmartFake + implements _i2.StreamedResponse { + _FakeStreamedResponse_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [Client]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockClient extends _i1.Mock implements _i2.Client { + @override + _i3.Future<_i2.Response> head( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #head, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> get( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #get, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> post( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #post, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> put( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #put, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> patch( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #patch, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future<_i2.Response> delete( + Uri? url, { + Map? headers, + Object? body, + _i4.Encoding? encoding, + }) => + (super.noSuchMethod( + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + returnValue: _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.Response>.value(_FakeResponse_0( + this, + Invocation.method( + #delete, + [url], + { + #headers: headers, + #body: body, + #encoding: encoding, + }, + ), + )), + ) as _i3.Future<_i2.Response>); + + @override + _i3.Future read( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + returnValueForMissingStub: + _i3.Future.value(_i5.dummyValue( + this, + Invocation.method( + #read, + [url], + {#headers: headers}, + ), + )), + ) as _i3.Future); + + @override + _i3.Future<_i6.Uint8List> readBytes( + Uri? url, { + Map? headers, + }) => + (super.noSuchMethod( + Invocation.method( + #readBytes, + [url], + {#headers: headers}, + ), + returnValue: _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + returnValueForMissingStub: + _i3.Future<_i6.Uint8List>.value(_i6.Uint8List(0)), + ) as _i3.Future<_i6.Uint8List>); + + @override + _i3.Future<_i2.StreamedResponse> send(_i2.BaseRequest? request) => + (super.noSuchMethod( + Invocation.method( + #send, + [request], + ), + returnValue: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + returnValueForMissingStub: + _i3.Future<_i2.StreamedResponse>.value(_FakeStreamedResponse_1( + this, + Invocation.method( + #send, + [request], + ), + )), + ) as _i3.Future<_i2.StreamedResponse>); + + @override + void close() => super.noSuchMethod( + Invocation.method( + #close, + [], + ), + returnValueForMissingStub: null, + ); +} diff --git a/test/src/analyzer/abstract_analyzer_test.dart b/test/src/analyzer/abstract_analyzer_test.dart new file mode 100644 index 00000000..fd5be6d9 --- /dev/null +++ b/test/src/analyzer/abstract_analyzer_test.dart @@ -0,0 +1,32 @@ +import 'package:flutter_code_editor/flutter_code_editor.dart' + show AbstractAnalyzer; +import 'package:flutter_code_editor/src/analyzer/models/analysis_result.dart' + show AnalysisResult; +import 'package:flutter_code_editor/src/analyzer/models/issue.dart' show Issue; +import 'package:flutter_code_editor/src/code/code.dart' show Code; +import 'package:flutter_test/flutter_test.dart'; + +class DummyAnalyzer extends AbstractAnalyzer { + const DummyAnalyzer(); + + @override + Future analyze(Code code) async { + return const AnalysisResult(issues: []); + } +} + +void main() { + group('AbstractAnalyzer', () { + test('dispose() can be called safely', () { + const DummyAnalyzer analyzer = DummyAnalyzer(); + analyzer.dispose(); + }); + + test('analyze() should return AnalysisResult with no issues', () async { + const DummyAnalyzer analyzer = DummyAnalyzer(); + final AnalysisResult result = await analyzer.analyze(Code(text: '')); + expect(result, isA()); + expect(result.issues, isEmpty); + }); + }); +} diff --git a/test/src/analyzer/dartpad_analyzer_test.dart b/test/src/analyzer/dartpad_analyzer_test.dart new file mode 100644 index 00000000..c720b488 --- /dev/null +++ b/test/src/analyzer/dartpad_analyzer_test.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:flutter_code_editor/flutter_code_editor.dart' + show DartPadAnalyzer; +import 'package:flutter_code_editor/src/analyzer/models/analysis_result.dart' + show AnalysisResult; +import 'package:flutter_code_editor/src/analyzer/models/issue.dart' show Issue; +import 'package:flutter_code_editor/src/analyzer/models/issue_type.dart' + show IssueType; +import 'package:flutter_code_editor/src/code/code.dart' show Code; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:mockito/mockito.dart'; + +import '../../mocks/general_mocks.mocks.dart' show MockClient; + +void main() { + group('DartPadAnalyzer', () { + late MockClient mockClient; + late DartPadAnalyzer analyzer; + + setUp(() { + mockClient = MockClient(); + analyzer = DartPadAnalyzer(client: mockClient); + }); + + test('returns empty issues when response is empty or invalid', () async { + when>( + mockClient.post( + Uri.parse(DartPadAnalyzer.url), + body: anyNamed('body'), + encoding: anyNamed('encoding'), + ), + ).thenAnswer( + (Invocation _) async => http.Response('{}', 200), + ); + + final AnalysisResult result = await analyzer.analyze(Code(text: '')); + + expect(result.issues, isEmpty); + }); + + test('parses issues correctly from response', () async { + final Map mockResponse = { + 'issues': >[ + { + 'line': 2, + 'message': 'Avoid using print', + 'kind': 'warning', + 'correction': 'Remove the print statement', + 'url': 'https://example.com/print', + }, + ], + }; + + when>( + mockClient.post( + Uri.parse(DartPadAnalyzer.url), + body: anyNamed('body'), + encoding: anyNamed('encoding'), + ), + ).thenAnswer( + (Invocation _) async => http.Response(jsonEncode(mockResponse), 200), + ); + + final AnalysisResult result = + await analyzer.analyze(Code(text: 'print("hi");')); + + expect(result.issues.length, 1); + final Issue issue = result.issues.first; + expect(issue.message, 'Avoid using print'); + expect(issue.type, IssueType.warning); + expect(issue.line, 1); // 2 - 1 + }); + }); +} diff --git a/test/src/analyzer/default_local_analyzer_test.dart b/test/src/analyzer/default_local_analyzer_test.dart new file mode 100644 index 00000000..8081732b --- /dev/null +++ b/test/src/analyzer/default_local_analyzer_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter_code_editor/flutter_code_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class TestCode implements Code { + final Code _inner; + final List _mockedInvalidBlocks; + + TestCode({ + required String text, + required List mockedInvalidBlocks, + }) : _mockedInvalidBlocks = mockedInvalidBlocks, + _inner = Code(text: text); // use the factory constructor + + @override + List get invalidBlocks => _mockedInvalidBlocks; + + // Forward everything else to the inner Code instance. + @override + dynamic noSuchMethod(Invocation invocation) => Function.apply( + _inner.noSuchMethod, + [invocation], + ); +} + +void main() { + group('DefaultLocalAnalyzer', () { + test('returns issues from overridden invalidBlocks', () async { + final InvalidFoldableBlock block1 = InvalidFoldableBlock( + type: FoldableBlockType.braces, + startLine: 1, + ); + + final InvalidFoldableBlock block2 = InvalidFoldableBlock( + type: FoldableBlockType.brackets, + endLine: 3, + ); + + final TestCode code = TestCode( + text: 'fake', + mockedInvalidBlocks: [block1, block2], + ); + + const DefaultLocalAnalyzer analyzer = DefaultLocalAnalyzer(); + final AnalysisResult result = await analyzer.analyze(code); + + expect(result.issues.length, 2); + expect(result.issues[0], equals(block1.issue)); + expect(result.issues[1].line, equals(3)); + }); + + test('returns empty issues when invalidBlocks is empty', () async { + final TestCode code = TestCode( + text: 'clean', + mockedInvalidBlocks: [], + ); + + const DefaultLocalAnalyzer analyzer = DefaultLocalAnalyzer(); + final AnalysisResult result = await analyzer.analyze(code); + + expect(result.issues, isEmpty); + }); + }); +} diff --git a/test/src/analyzer/models/analysis_result_test.dart b/test/src/analyzer/models/analysis_result_test.dart new file mode 100644 index 00000000..a78f992a --- /dev/null +++ b/test/src/analyzer/models/analysis_result_test.dart @@ -0,0 +1,21 @@ +import 'package:flutter_code_editor/flutter_code_editor.dart' show Issue; +import 'package:flutter_code_editor/src/analyzer/models/analysis_result.dart' + show AnalysisResult; +import 'package:flutter_code_editor/src/analyzer/models/issue_type.dart' + show IssueType; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('AnalysisResult', () { + test('should create an AnalysisResult with a list of issues', () { + final List issues = [ + const Issue(line: 1, message: 'Test', type: IssueType.info), + ]; + + final AnalysisResult result = AnalysisResult(issues: issues); + + expect(result.issues, hasLength(1)); + expect(result.issues.first.message, 'Test'); + }); + }); +} diff --git a/test/src/analyzer/models/issue_test.dart b/test/src/analyzer/models/issue_test.dart new file mode 100644 index 00000000..727b45ae --- /dev/null +++ b/test/src/analyzer/models/issue_test.dart @@ -0,0 +1,73 @@ +import 'package:flutter_code_editor/flutter_code_editor.dart' show Issue; +import 'package:flutter_code_editor/src/analyzer/models/issue.dart' + show issueLineComparator; +import 'package:flutter_code_editor/src/analyzer/models/issue_type.dart' + show IssueType; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group( + 'Issue Model Tests', + () { + test( + 'should create an Issue with required fields', + () { + const Issue issue = Issue( + line: 5, + message: 'Unexpected semicolon', + type: IssueType.warning, + ); + + expect(issue.line, 5); + expect(issue.message, 'Unexpected semicolon'); + expect(issue.type, IssueType.warning); + expect(issue.suggestion, isNull); + expect(issue.url, isNull); + }, + ); + + test( + 'should create an Issue with all fields', + () { + const Issue issue = Issue( + line: 10, + message: 'Missing return type', + type: IssueType.error, + suggestion: 'Add a return type', + url: 'https://example.com/return-type', + ); + + expect(issue.line, 10); + expect(issue.message, 'Missing return type'); + expect(issue.type, IssueType.error); + expect(issue.suggestion, 'Add a return type'); + expect(issue.url, 'https://example.com/return-type'); + }, + ); + }, + ); + + group( + 'issueLineComparator', + () { + test( + 'should compare issues by line number', + () { + const Issue issue1 = + Issue(line: 3, message: '', type: IssueType.info); + const Issue issue2 = + Issue(line: 5, message: '', type: IssueType.info); + const Issue issue3 = + Issue(line: 1, message: '', type: IssueType.info); + + final List issues = [issue1, issue2, issue3]; + issues.sort(issueLineComparator); + + expect(issues[0].line, 1); + expect(issues[1].line, 3); + expect(issues[2].line, 5); + }, + ); + }, + ); +} From edd58a141f44818c8fc4cd5991a05f82a697d2cb Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:44:21 +0200 Subject: [PATCH 12/16] add testing annotation --- lib/src/analyzer/dartpad_analyzer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/analyzer/dartpad_analyzer.dart b/lib/src/analyzer/dartpad_analyzer.dart index 62e54852..8c64e70c 100644 --- a/lib/src/analyzer/dartpad_analyzer.dart +++ b/lib/src/analyzer/dartpad_analyzer.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import '../code/code.dart'; @@ -10,6 +11,7 @@ import 'models/issue_type.dart'; // Example for implementation of Analyzer for Dart. class DartPadAnalyzer extends AbstractAnalyzer { + @visibleForTesting static const String url = 'https://stable.api.dartpad.dev/api/dartservices/v2/analyze'; From 75efa36baa93f8dbd1f71042cd546186f6d534eb Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:48:16 +0200 Subject: [PATCH 13/16] Downgrade mockito --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 2df65a73..df1ea1a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dev_dependencies: build_runner: ^2.4.15 fake_async: ^1.3.1 flutter_test: { sdk: flutter } - mockito: ^5.4.5 + mockito: ^5.4.4 total_lints: ^3.1.1 flutter: From 583a2469b5f2d80c0fb7fe88f0a57aae28cf4754 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:50:00 +0200 Subject: [PATCH 14/16] downgrade build runner --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index df1ea1a3..f1dde93e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,7 +25,7 @@ dependencies: url_launcher: ^6.1.8 dev_dependencies: - build_runner: ^2.4.15 + build_runner: ^2.4.9 fake_async: ^1.3.1 flutter_test: { sdk: flutter } mockito: ^5.4.4 From 4da989c21fedf1d344f647a7a053291d9e22306a Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:58:14 +0200 Subject: [PATCH 15/16] =?UTF-8?q?Use=20correct=20flutter=20version=20when?= =?UTF-8?q?=20coding=20=F0=9F=A4=A6=E2=80=8D=E2=99=82=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/code_field/code_field.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/code_field/code_field.dart b/lib/src/code_field/code_field.dart index 9a249523..e0281789 100644 --- a/lib/src/code_field/code_field.dart +++ b/lib/src/code_field/code_field.dart @@ -535,11 +535,11 @@ class _CodeFieldState extends State { } double _getCaretHeight(TextPainter textPainter) { - final double caretFullHeight = textPainter.getFullHeightForCaret( + final double? caretFullHeight = textPainter.getFullHeightForCaret( widget.controller.selection.base, Rect.zero, ); - return caretFullHeight; + return caretFullHeight ?? 0; } double _getPopupLeftOffset(TextPainter textPainter) { From f010da79d67ac48e268ab09f34e97800b612c1d6 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:58:21 +0200 Subject: [PATCH 16/16] Update general_mocks.mocks.dart --- test/mocks/general_mocks.mocks.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/mocks/general_mocks.mocks.dart b/test/mocks/general_mocks.mocks.dart index 2c8d70f0..800811d0 100644 --- a/test/mocks/general_mocks.mocks.dart +++ b/test/mocks/general_mocks.mocks.dart @@ -1,4 +1,4 @@ -// Mocks generated by Mockito 5.4.5 from annotations +// Mocks generated by Mockito 5.4.4 from annotations // in flutter_code_editor/test/mocks/general_mocks.dart. // Do not manually edit this file. @@ -19,7 +19,6 @@ import 'package:mockito/src/dummies.dart' as _i5; // ignore_for_file: deprecated_member_use_from_same_package // ignore_for_file: implementation_imports // ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable // ignore_for_file: prefer_const_constructors // ignore_for_file: unnecessary_parenthesis // ignore_for_file: camel_case_types