Skip to content

enhancement/Add Tests + Fix State Management Issues (for web mainly) + Small Refactors #296

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
targets:
$default:
sources:
- $package$
- lib/$lib$
- lib/**.dart
- test/**.dart
builders:
mockito|mockBuilder:
generate_for:
- test/**.dart
38 changes: 21 additions & 17 deletions lib/src/analyzer/dartpad_analyzer.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// ignore_for_file: avoid_dynamic_calls
import 'dart:convert';

import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

import '../code/code.dart';
Expand All @@ -11,41 +11,45 @@ import 'models/issue_type.dart';

// Example for implementation of Analyzer for Dart.
class DartPadAnalyzer extends AbstractAnalyzer {
static const _url =
@visibleForTesting
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<AnalysisResult> 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(<String, String>{'source': code.text}),
encoding: utf8,
);

final decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map;
final issueMaps = decodedResponse['issues'];
final Map<String, dynamic> decodedResponse =
jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>;
final dynamic issueMaps = decodedResponse['issues'];

if (issueMaps is! Iterable || (issueMaps.isEmpty)) {
return const AnalysisResult(issues: []);
if (issueMaps is! Iterable<dynamic> || (issueMaps.isEmpty)) {
return const AnalysisResult(issues: <Issue>[]);
}

final issues = issueMaps
final List<Issue> issues = issueMaps
.cast<Map<String, dynamic>>()
.map(issueFromJson)
.map<Issue>(issueFromJson)
.toList(growable: false);

return AnalysisResult(issues: issues);
}
}

// Converts json to Issue object for the DartAnalyzer.
Issue issueFromJson(Map<String, dynamic> 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,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/analyzer/models/issue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ class Issue {
});
}

Comparator<Issue> issueLineComparator = (issue1, issue2) {
Comparator<Issue> issueLineComparator = (Issue issue1, Issue issue2) {
return issue1.line - issue2.line;
};
94 changes: 54 additions & 40 deletions lib/src/code_field/code_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -214,11 +214,15 @@ class CodeField extends StatefulWidget {

class _CodeFieldState extends State<CodeField> {
// 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<State<StatefulWidget>> _codeFieldKey =
GlobalKey<State<StatefulWidget>>();

OverlayEntry? _suggestionsPopup;
OverlayEntry? _searchPopup;
Expand All @@ -234,15 +238,16 @@ class _CodeFieldState extends State<CodeField> {
late TextStyle textStyle;
Color? _backgroundCol;

final _editorKey = GlobalKey();
final GlobalKey<State<StatefulWidget>> _editorKey =
GlobalKey<State<StatefulWidget>>();
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);
Expand All @@ -261,11 +266,8 @@ class _CodeFieldState extends State<CodeField> {
disableSpellCheckIfWeb();

WidgetsBinding.instance.addPostFrameCallback((_) {
final double width = _codeFieldKey.currentContext!.size!.width;
final double height = _codeFieldKey.currentContext!.size!.height;
windowSize = Size(width, height);
_onTextChanged();
});
_onTextChanged();
}

KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) {
Expand All @@ -284,9 +286,9 @@ class _CodeFieldState extends State<CodeField> {
);
_searchPopup?.remove();
_searchPopup = null;
_numberScroll?.dispose();
_codeScroll?.dispose();
_horizontalCodeScroll?.dispose();
_numberScroll.dispose();
_codeScroll.dispose();
_horizontalCodeScroll.dispose();
super.dispose();
}

Expand All @@ -310,18 +312,20 @@ class _CodeFieldState extends State<CodeField> {
}

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) {
if (mounted) {
setState(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// For some reason _codeFieldKey.currentContext is null in tests
// so check first.
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);
}
});
});
});
}
}

void _onTextChanged() {
Expand All @@ -339,12 +343,15 @@ class _CodeFieldState extends State<CodeField> {
if (line.length > longestLine.length) longestLine = line;
});

if (_codeScroll != null && _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!;
if (_codeScroll.hasClients) {
fixedOffset += Offset(0, _codeScroll.offset);
}
_editorOffset = fixedOffset;
}
}
Expand All @@ -362,7 +369,7 @@ class _CodeFieldState extends State<CodeField> {
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 0,
Expand Down Expand Up @@ -391,10 +398,10 @@ class _CodeFieldState extends State<CodeField> {
@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<String, TextStyle>? styles = CodeTheme.of(context)?.styles;
_backgroundCol = widget.background ??
styles?[rootKey]?.backgroundColor ??
DefaultStyles.backgroundColor;
Expand All @@ -410,7 +417,7 @@ class _CodeFieldState extends State<CodeField> {

textStyle = defaultTextStyle.merge(widget.textStyle);

final codeField = TextField(
final TextField codeField = TextField(
focusNode: _focusNode,
scrollPadding: widget.padding,
style: textStyle,
Expand Down Expand Up @@ -505,10 +512,12 @@ class _CodeFieldState extends State<CodeField> {
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) {
Expand All @@ -534,22 +543,27 @@ class _CodeFieldState extends State<CodeField> {
}

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,
);
}

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,
);
Expand Down Expand Up @@ -589,7 +603,7 @@ class _CodeFieldState extends State<CodeField> {

OverlayEntry _buildSearchOverlay() {
final colorScheme = Theme.of(context).colorScheme;
final borderColor = _getTextColorFromTheme() ?? colorScheme.onBackground;
final borderColor = _getTextColorFromTheme() ?? colorScheme.onSurface;
return OverlayEntry(
builder: (context) {
return Positioned(
Expand Down
34 changes: 19 additions & 15 deletions lib/src/gutter/error.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,32 @@ class _GutterErrorWidgetState extends State<GutterErrorWidget> {
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
// if mouse has exited the icon and entered popup.
Future.delayed(
const Duration(milliseconds: 50),
() {
setState(() {
if (!_mouseEnteredPopup) {
_entry?.remove();
_entry = null;
}
});
if (mounted) {
setState(() {
if (!_mouseEnteredPopup) {
_entry?.remove();
_entry = null;
}
});
}
},
);
},
Expand Down
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ dependencies:
url_launcher: ^6.1.8

dev_dependencies:
build_runner: ^2.4.9
fake_async: ^1.3.1
flutter_test: { sdk: flutter }
mockito: ^5.4.4
total_lints: ^3.1.1

flutter:
Expand Down
14 changes: 14 additions & 0 deletions test/mocks/general_mocks.dart
Original file line number Diff line number Diff line change
@@ -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<dynamic>>[
MockSpec<http.Client>(),
],
)
void main() {}
Loading