diff --git a/pkgs/dartpad_ui/lib/docs.dart b/pkgs/dartpad_ui/lib/docs.dart index 2e07eb3cd..769ee1d7c 100644 --- a/pkgs/dartpad_ui/lib/docs.dart +++ b/pkgs/dartpad_ui/lib/docs.dart @@ -9,13 +9,14 @@ import 'package:url_launcher/url_launcher.dart' as url_launcher; import 'model.dart'; import 'theme.dart'; -import 'widgets.dart'; class DocsWidget extends StatefulWidget { final AppModel appModel; + final DocumentResponse documentResponse; const DocsWidget({ required this.appModel, + required this.documentResponse, super.key, }); @@ -27,94 +28,33 @@ class _DocsWidgetState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - - return Container( - decoration: BoxDecoration( - color: theme.scaffoldBackgroundColor, - border: Border( - top: Divider.createBorderSide( - context, - width: 8.0, - color: theme.colorScheme.surface, - ), - ), - ), - padding: const EdgeInsets.all(denseSpacing), - child: Stack( - children: [ - ValueListenableBuilder( - valueListenable: widget.appModel.currentDocs, - builder: (context, DocumentResponse? docs, _) { - // TODO: Consider showing propagatedType if not null. - - var title = _cleanUpTitle(docs?.elementDescription); - if (docs?.deprecated == true) { - title = '$title (deprecated)'; - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (title.isNotEmpty) - Text( - title, - style: theme.textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - if (title.isNotEmpty) const SizedBox(height: denseSpacing), - Expanded( - child: Markdown( - data: docs?.dartdoc ?? '', - padding: const EdgeInsets.only(left: denseSpacing), - onTapLink: _handleMarkdownTap, - ), - ), - ], - ); - }, + final docs = widget.documentResponse; + + final title = docs.cleanedUpTitle ?? ''; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + if (title.isNotEmpty) + Text( + title, + style: theme.textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - Padding( - padding: const EdgeInsets.all(denseSpacing), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MiniIconButton( - icon: Icons.close, - tooltip: 'Close', - onPressed: _closePanel, - small: true, - ) - ], - ), + if (title.isNotEmpty) const SizedBox(height: denseSpacing), + Expanded( + child: Markdown( + data: docs.dartdoc ?? '', + padding: const EdgeInsets.only(left: denseSpacing), + onTapLink: _handleMarkdownTap, ), - ], - ), + ), + ], ); } - void _closePanel() { - widget.appModel.docsShowing.value = false; - } - - String _cleanUpTitle(String? title) { - if (title == null) return ''; - - // "(new) Text(\n String data, {\n Key? key,\n ... selectionColor,\n})" - - // Remove ws right after method args. - title = title.replaceAll('(\n ', '('); - - // Remove ws before named args. - title = title.replaceAll('{\n ', '{'); - - // Remove ws after named args. - title = title.replaceAll(',\n}', '}'); - - return title.replaceAll('\n', '').replaceAll(' ', ' '); - } - void _handleMarkdownTap(String text, String? href, String title) { if (href != null) { final uri = Uri.tryParse(href); @@ -126,3 +66,31 @@ class _DocsWidgetState extends State { } } } + +extension DocumentResponseExtension on DocumentResponse { + String? get cleanedUpTitle { + if (elementDescription == null) { + return null; + } else { + // "(new) Text(\n String data, {\n Key? key,\n ... selectionColor,\n})" + var title = elementDescription!; + + // Remove ws right after method args. + title = title.replaceAll('(\n ', '('); + + // Remove ws before named args. + title = title.replaceAll('{\n ', '{'); + + // Remove ws after named args. + title = title.replaceAll(',\n}', '}'); + + title = title.replaceAll('\n', '').replaceAll(' ', ' '); + + if (this.deprecated == true) { + title = '$title (deprecated)'; + } + + return title; + } + } +} diff --git a/pkgs/dartpad_ui/lib/editor/editor.dart b/pkgs/dartpad_ui/lib/editor/editor.dart index 50b918919..445126f37 100644 --- a/pkgs/dartpad_ui/lib/editor/editor.dart +++ b/pkgs/dartpad_ui/lib/editor/editor.dart @@ -225,14 +225,6 @@ class _EditorWidgetState extends State implements EditorService { }.toJS, ); - codeMirror!.on( - 'mousedown', - ([JSAny? _, JSAny? __]) { - // Delay slightly to allow codemirror to update the cursor position. - Timer.run(() => appModel.lastEditorClickOffset.value = cursorOffset); - }.toJS, - ); - appModel.sourceCodeController.addListener(_updateCodemirrorFromModel); appModel.analysisIssues .addListener(() => _updateIssues(appModel.analysisIssues.value)); diff --git a/pkgs/dartpad_ui/lib/main.dart b/pkgs/dartpad_ui/lib/main.dart index 9dc4f3328..a2b43a540 100644 --- a/pkgs/dartpad_ui/lib/main.dart +++ b/pkgs/dartpad_ui/lib/main.dart @@ -234,7 +234,6 @@ class _DartPadMainPageState extends State const ValueKey('loading-overlay-widget'); final ValueKey _editorKey = const ValueKey('editor'); final ValueKey _consoleKey = const ValueKey('console'); - final ValueKey _docsKey = const ValueKey('docs'); final ValueKey _tabBarKey = const ValueKey('tab-bar'); final ValueKey _executionStackKey = const ValueKey('execution-stack'); final ValueKey _scaffoldKey = const ValueKey('scaffold'); @@ -287,12 +286,10 @@ class _DartPadMainPageState extends State }); appModel.compilingBusy.addListener(_handleRunStarted); - appModel.lastEditorClickOffset.addListener(_handleDocClicked); } @override void dispose() { - appModel.lastEditorClickOffset.removeListener(_handleDocClicked); appModel.compilingBusy.removeListener(_handleRunStarted); appServices.dispose(); @@ -324,32 +321,6 @@ class _DartPadMainPageState extends State key: _editorKey, ); - final editingGroup = ValueListenableBuilder( - valueListenable: appModel.docsShowing, - builder: (context, bool docsShowing, _) { - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final height = constraints.maxHeight; - final editorHeight = docsShowing ? height * dividerSplit : height; - final docsHeight = - docsShowing ? height * (1.0 - dividerSplit) : 0.0; - - return Column( - children: [ - SizedBox(height: editorHeight, child: editor), - SizedBox( - height: docsHeight, - child: DocsWidget( - appModel: appModel, - key: _docsKey, - ), - ), - ], - ); - }, - ); - }); - final tabBar = TabBar( controller: tabController, tabs: const [ @@ -414,7 +385,7 @@ class _DartPadMainPageState extends State child: IndexedStack( index: tabController.index, children: [ - editingGroup, + editor, executionStack, ], ), @@ -446,7 +417,7 @@ class _DartPadMainPageState extends State gripSize: defaultGripSize, controller: mainSplitter, children: [ - editingGroup, + editor, executionStack, ], ), @@ -541,50 +512,6 @@ class _DartPadMainPageState extends State } }); } - - static final RegExp identifierChar = RegExp(r'[\w\d_<=>]'); - - void _handleDocClicked() async { - // TODO(devoncarew): Disable opening the documentation panel; this is too - // disruptive when people are editing code. We should switch to using a - // tooltip (https://github.com/dart-lang/dart-pad/issues/3032). - return; - - // ignore: dead_code - try { - final source = appModel.sourceCodeController.text; - final offset = appModel.lastEditorClickOffset.value; - - var valid = true; - - if (offset < 0 || offset >= source.length) { - valid = false; - } else { - valid = identifierChar.hasMatch(source.substring(offset, offset + 1)); - } - - if (!valid) { - appModel.docsShowing.value = false; - appModel.currentDocs.value = null; - return; - } - - final result = await appServices.document( - SourceRequest(source: source, offset: offset), - ); - - if (result.elementKind == null) { - appModel.docsShowing.value = false; - appModel.currentDocs.value = null; - } else { - appModel.currentDocs.value = result; - appModel.docsShowing.value = true; - } - } on ApiRequestError { - appModel.editorStatus.showToast('Error retrieving docs'); - return; - } - } } class LoadingOverlay extends StatelessWidget { @@ -746,6 +673,22 @@ class EditorWithButtons extends StatelessWidget { textDirection: TextDirection.ltr, crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Dartdoc help button + ValueListenableBuilder( + valueListenable: appModel.docHelpBusy, + builder: (_, bool value, __) { + return PointerInterceptor( + child: MiniIconButton( + icon: Icons.help_outline, + tooltip: 'Show docs', + // small: true, + onPressed: + value ? null : () => _showDocs(context), + ), + ); + }, + ), + const SizedBox(width: denseSpacing), // Format action ValueListenableBuilder( valueListenable: appModel.formattingBusy, @@ -795,6 +738,63 @@ class EditorWithButtons extends StatelessWidget { ], ); } + + static final RegExp identifierChar = RegExp(r'[\w\d_<=>]'); + + void _showDocs(BuildContext context) async { + try { + final source = appModel.sourceCodeController.text; + final offset = appServices.editorService?.cursorOffset ?? -1; + + var valid = true; + if (offset < 0 || offset >= source.length) { + valid = false; + } else { + valid = identifierChar.hasMatch(source.substring(offset, offset + 1)); + } + + if (!valid) { + appModel.editorStatus.showToast('No docs at location.'); + return; + } + + final result = await appServices.document( + SourceRequest(source: source, offset: offset), + ); + + if (result.elementKind == null) { + appModel.editorStatus.showToast('No docs at location.'); + return; + } else if (context.mounted) { + // show result + + showDialog( + context: context, + builder: (context) { + const longTitle = 40; + + var title = result.cleanedUpTitle ?? 'Dartdoc'; + if (title.length > longTitle) { + title = '${title.substring(0, longTitle)}…'; + } + return MediumDialog( + title: title, + child: DocsWidget( + appModel: appModel, + documentResponse: result, + ), + ); + }, + ); + } + + appServices.editorService!.focus(); + } catch (error) { + appModel.editorStatus.showToast('Error retrieving docs'); + appModel.appendLineToConsole('$error'); + return; + } + } } class StatusLineWidget extends StatelessWidget { @@ -1124,6 +1124,7 @@ class OverflowMenu extends StatelessWidget { class ContinueInMenu extends StatelessWidget { final VoidCallback openInIdx; + const ContinueInMenu({super.key, required this.openInIdx}); @override diff --git a/pkgs/dartpad_ui/lib/model.dart b/pkgs/dartpad_ui/lib/model.dart index eaf6a292b..a14e1badb 100644 --- a/pkgs/dartpad_ui/lib/model.dart +++ b/pkgs/dartpad_ui/lib/model.dart @@ -52,6 +52,7 @@ class AppModel { final ValueNotifier formattingBusy = ValueNotifier(false); final ValueNotifier compilingBusy = ValueNotifier(false); + final ValueNotifier docHelpBusy = ValueNotifier(false); final StatusController editorStatus = StatusController(); @@ -60,15 +61,6 @@ class AppModel { final ValueNotifier _layoutMode = ValueNotifier(LayoutMode.both); ValueListenable get layoutMode => _layoutMode; - /// Whether the docs panel is showing or should show. - final ValueNotifier docsShowing = ValueNotifier(false); - - /// The last document request received. - final ValueNotifier currentDocs = ValueNotifier(null); - - /// Used to pass information about mouse clicks in the editor. - final ValueNotifier lastEditorClickOffset = ValueNotifier(0); - final ValueNotifier splitViewDragState = ValueNotifier(SplitDragState.inactive); diff --git a/pkgs/dartpad_ui/lib/widgets.dart b/pkgs/dartpad_ui/lib/widgets.dart index 6f6d784cc..7100678cd 100644 --- a/pkgs/dartpad_ui/lib/widgets.dart +++ b/pkgs/dartpad_ui/lib/widgets.dart @@ -205,7 +205,7 @@ class MediumDialog extends StatelessWidget { return PointerInterceptor( child: AlertDialog( backgroundColor: Theme.of(context).scaffoldBackgroundColor, - title: Text(title), + title: Text(title, maxLines: 1), contentTextStyle: Theme.of(context).textTheme.bodyMedium, contentPadding: const EdgeInsets.fromLTRB(24, defaultSpacing, 24, 8), content: Column(