Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix error reporting #3162

Open
wants to merge 9 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
10 changes: 10 additions & 0 deletions pkgs/dart_services/lib/src/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void main() {
// This code should be kept up-to-date with
// https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/web/bootstrap.dart#L236.
const kBootstrapFlutterCode = r'''
import 'package:flutter/foundation.dart';
import 'dart:ui_web' as ui_web;
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
Expand All @@ -29,7 +30,16 @@ import 'main.dart' as entrypoint;
@JS('window')
external JSObject get _window;

@JS('reportFlutterError')
external void _reportFlutterError(String message);

Future<void> main() async {
// Capture errors and throw them to the JS console so DartPad can report them
// correctly.
FlutterError.onError = (details) {
_reportFlutterError(details.toString());
};

// Mock DWDS indicators to allow Flutter to register hot reload 'reassemble'
// extension.
_window[r'$dwdsVersion'] = true.toJS;
Expand Down
101 changes: 53 additions & 48 deletions pkgs/dartpad_ui/lib/console.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import 'enable_gen_ai.dart';
import 'model.dart';
import 'suggest_fix.dart';
import 'theme.dart';
import 'utils.dart';
import 'widgets.dart';

class ConsoleWidget extends StatefulWidget {
final bool showDivider;
final ValueNotifier<String> output;
final ConsoleNotifier output;

const ConsoleWidget({
this.showDivider = true,
this.showDivider = false,
required this.output,
super.key,
});
Expand Down Expand Up @@ -64,62 +65,66 @@ class _ConsoleWidgetState extends State<ConsoleWidget> {
: null,
),
padding: const EdgeInsets.all(denseSpacing),
child: ValueListenableBuilder(
valueListenable: widget.output,
builder:
(context, consoleOutput, _) => Stack(
children: [
SizedBox.expand(
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
consoleOutput,
maxLines: null,
style: GoogleFonts.robotoMono(
fontSize: theme.textTheme.bodyMedium?.fontSize,
),
child: ListenableBuilder(
listenable: widget.output,
builder: (context, _) {
return Stack(
children: [
SizedBox.expand(
child: SingleChildScrollView(
controller: scrollController,
child: SelectableText(
widget.output.valueToDisplay,
maxLines: null,
style: GoogleFonts.robotoMono(
fontSize: theme.textTheme.bodyMedium?.fontSize,
color: switch (widget.output.hasError) {
false => theme.textTheme.bodyMedium?.color,
true => theme.colorScheme.error.darker,
},
),
),
),
Padding(
padding: const EdgeInsets.all(denseSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (genAiEnabled && appModel.consoleShowingError)
MiniIconButton(
icon: Image.asset(
'gemini_sparkle_192.png',
width: 16,
height: 16,
),
tooltip: 'Suggest fix',
onPressed:
() => suggestFix(
context: context,
appType: appModel.appType,
errorMessage: consoleOutput,
),
),
),
Padding(
padding: const EdgeInsets.all(denseSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (genAiEnabled && appModel.consoleShowingError)
MiniIconButton(
icon: const Icon(Icons.playlist_remove),
tooltip: 'Clear console',
onPressed: consoleOutput.isEmpty ? null : _clearConsole,
icon: Image.asset(
'gemini_sparkle_192.png',
width: 16,
height: 16,
),
tooltip: 'Suggest fix',
onPressed:
() => suggestFix(
context: context,
appType: appModel.appType,
errorMessage: widget.output.error,
),
),
],
),
MiniIconButton(
icon: const Icon(Icons.playlist_remove),
tooltip: 'Clear console',
onPressed:
widget.output.isEmpty
? null
: () => widget.output.clear(),
),
],
),
],
),
),
],
);
},
),
);
}

void _clearConsole() {
widget.output.value = '';
}

void _scrollToEnd() {
if (!mounted) return;
final controller = scrollController;
Expand Down
7 changes: 6 additions & 1 deletion pkgs/dartpad_ui/lib/execution/frame.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import '../model.dart';
class ExecutionServiceImpl implements ExecutionService {
final StreamController<String> _stdoutController =
StreamController<String>.broadcast();
final StreamController<String> _stderrController =
StreamController<String>.broadcast();

web.HTMLIFrameElement _frame;
late String _frameSrc;
Expand Down Expand Up @@ -53,6 +55,9 @@ class ExecutionServiceImpl implements ExecutionService {
@override
Stream<String> get onStdout => _stdoutController.stream;

@override
Stream<String> get onStderr => _stderrController.stream;

@override
set ignorePointer(bool ignorePointer) {
_frame.style.pointerEvents = ignorePointer ? 'none' : 'auto';
Expand Down Expand Up @@ -254,7 +259,7 @@ require(["dartpad_main", "dart_sdk"], function(dartpad_main, dart_sdk) {
// Ignore any exceptions before the iframe has completed
// initialization.
if (_readyCompleter.isCompleted) {
_stdoutController.add(data['message'] as String);
_stderrController.add(data['message'] as String);
}
} else if (type == 'ready' && !_readyCompleter.isCompleted) {
_readyCompleter.complete();
Expand Down
68 changes: 42 additions & 26 deletions pkgs/dartpad_ui/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,12 @@ class _DartPadMainPageState extends State<DartPadMainPage>
late final AppModel appModel;
late final AppServices appServices;
late final SplitViewController mainSplitter;
late final SplitViewController consoleSplitter;
late final TabController tabController;

final Key _executionWidgetKey = GlobalKey(debugLabel: 'execution-widget');
final GlobalKey _executionWidgetKey = GlobalKey(
debugLabel: 'execution-widget',
);
final ValueKey<String> _loadingOverlayKey = const ValueKey(
'loading-overlay-widget',
);
Expand All @@ -259,6 +262,13 @@ class _DartPadMainPageState extends State<DartPadMainPage>
appModel.splitDragStateManager.handleSplitChanged();
});

consoleSplitter = SplitViewController(
weights: [0.7, 0.3],
limits: [WeightLimit(max: 0.9), WeightLimit(min: 0.1)],
)..addListener(() {
appModel.splitDragStateManager.handleSplitChanged();
});

final channel =
widget.initialChannel != null
? Channel.forName(widget.initialChannel!)
Expand Down Expand Up @@ -338,28 +348,34 @@ class _DartPadMainPageState extends State<DartPadMainPage>
ValueListenableBuilder(
valueListenable: appModel.layoutMode,
builder: (context, LayoutMode mode, _) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final domHeight = mode.calcDomHeight(constraints.maxHeight);
final consoleHeight = mode.calcConsoleHeight(
constraints.maxHeight,
);

return Column(
children: [
SizedBox(height: domHeight, child: executionWidget),
SizedBox(
height: consoleHeight,
child: ConsoleWidget(
output: appModel.consoleOutput,
showDivider: mode == LayoutMode.both,
key: _consoleKey,
),
return switch (mode) {
LayoutMode.both => SplitView(
viewMode: SplitViewMode.Vertical,
gripColor: theme.colorScheme.surface,
gripColorActive: theme.colorScheme.surface,
gripSize: defaultGripSize,
controller: consoleSplitter,
children: [
executionWidget,
ConsoleWidget(
key: _consoleKey,
output: appModel.consoleNotifier,
),
],
),
LayoutMode.justDom => executionWidget,
LayoutMode.justConsole => Column(
children: [
SizedBox(height: 0, width: 0, child: executionWidget),
Expanded(
child: ConsoleWidget(
key: _consoleKey,
output: appModel.consoleNotifier,
),
],
);
},
);
),
],
),
};
},
),
loadingOverlay,
Expand Down Expand Up @@ -495,7 +511,7 @@ class _DartPadMainPageState extends State<DartPadMainPage>
appServices.editorService!.focus();
} catch (error) {
appModel.editorStatus.showToast('Error formatting code');
appModel.appendLineToConsole('Formatting issue: $error');
appModel.appendError('Formatting issue: $error');
return;
}
}
Expand Down Expand Up @@ -709,7 +725,7 @@ class DartPadAppBar extends StatelessWidget implements PreferredSizeWidget {
appServices.performCompileAndReloadOrRun();
} catch (error) {
appModel.editorStatus.showToast('Error generating code');
appModel.appendLineToConsole('Generating code issue: $error');
appModel.appendError('Generating code issue: $error');
}
}

Expand Down Expand Up @@ -782,7 +798,7 @@ class DartPadAppBar extends StatelessWidget implements PreferredSizeWidget {
appServices.performCompileAndReloadOrRun();
} catch (error) {
appModel.editorStatus.showToast('Error updating code');
appModel.appendLineToConsole('Updating code issue: $error');
appModel.appendError('Updating code issue: $error');
}
}
}
Expand Down Expand Up @@ -957,7 +973,7 @@ class EditorWithButtons extends StatelessWidget {
appServices.editorService!.focus();
} catch (error) {
appModel.editorStatus.showToast('Error retrieving docs');
appModel.appendLineToConsole('$error');
appModel.appendError('$error');
return;
}
}
Expand Down
Loading