Skip to content

Commit

Permalink
show dartdoc documentation in a model dialog (#3047)
Browse files Browse the repository at this point in the history
  • Loading branch information
devoncarew authored Aug 27, 2024
1 parent c825634 commit 8eb721a
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 177 deletions.
136 changes: 52 additions & 84 deletions pkgs/dartpad_ui/lib/docs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});

Expand All @@ -27,94 +28,33 @@ class _DocsWidgetState extends State<DocsWidget> {
@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);
Expand All @@ -126,3 +66,31 @@ class _DocsWidgetState extends State<DocsWidget> {
}
}
}

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;
}
}
}
8 changes: 0 additions & 8 deletions pkgs/dartpad_ui/lib/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -225,14 +225,6 @@ class _EditorWidgetState extends State<EditorWidget> 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));
Expand Down
151 changes: 76 additions & 75 deletions pkgs/dartpad_ui/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,6 @@ class _DartPadMainPageState extends State<DartPadMainPage>
const ValueKey('loading-overlay-widget');
final ValueKey<String> _editorKey = const ValueKey('editor');
final ValueKey<String> _consoleKey = const ValueKey('console');
final ValueKey<String> _docsKey = const ValueKey('docs');
final ValueKey<String> _tabBarKey = const ValueKey('tab-bar');
final ValueKey<String> _executionStackKey = const ValueKey('execution-stack');
final ValueKey<String> _scaffoldKey = const ValueKey('scaffold');
Expand Down Expand Up @@ -287,12 +286,10 @@ class _DartPadMainPageState extends State<DartPadMainPage>
});

appModel.compilingBusy.addListener(_handleRunStarted);
appModel.lastEditorClickOffset.addListener(_handleDocClicked);
}

@override
void dispose() {
appModel.lastEditorClickOffset.removeListener(_handleDocClicked);
appModel.compilingBusy.removeListener(_handleRunStarted);

appServices.dispose();
Expand Down Expand Up @@ -324,32 +321,6 @@ class _DartPadMainPageState extends State<DartPadMainPage>
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 [
Expand Down Expand Up @@ -414,7 +385,7 @@ class _DartPadMainPageState extends State<DartPadMainPage>
child: IndexedStack(
index: tabController.index,
children: [
editingGroup,
editor,
executionStack,
],
),
Expand Down Expand Up @@ -446,7 +417,7 @@ class _DartPadMainPageState extends State<DartPadMainPage>
gripSize: defaultGripSize,
controller: mainSplitter,
children: [
editingGroup,
editor,
executionStack,
],
),
Expand Down Expand Up @@ -541,50 +512,6 @@ class _DartPadMainPageState extends State<DartPadMainPage>
}
});
}

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 {
Expand Down Expand Up @@ -746,6 +673,22 @@ class EditorWithButtons extends StatelessWidget {
textDirection: TextDirection.ltr,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Dartdoc help button
ValueListenableBuilder<bool>(
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<bool>(
valueListenable: appModel.formattingBusy,
Expand Down Expand Up @@ -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<void>(
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 {
Expand Down Expand Up @@ -1124,6 +1124,7 @@ class OverflowMenu extends StatelessWidget {

class ContinueInMenu extends StatelessWidget {
final VoidCallback openInIdx;

const ContinueInMenu({super.key, required this.openInIdx});

@override
Expand Down
Loading

0 comments on commit 8eb721a

Please sign in to comment.