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

show api documentation on mouse click #2929

Merged
merged 2 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
123 changes: 123 additions & 0 deletions pkgs/sketch_pad/lib/docs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:dartpad_shared/model.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
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;

const DocsWidget({
required this.appModel,
super.key,
});

@override
State<DocsWidget> createState() => _DocsWidgetState();
}

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);
devoncarew marked this conversation as resolved.
Show resolved Hide resolved
if (docs?.deprecated == true) {
title = '$title (deprecated)';
}

// TODO: should the markdown text be selectable?
devoncarew marked this conversation as resolved.
Show resolved Hide resolved
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: theme.textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: denseSpacing),
Expanded(
child: Markdown(
data: docs?.dartdoc ?? '',
padding: const EdgeInsets.only(left: denseSpacing),
onTapLink: _handleMarkdownTap,
),
),
],
);
},
),
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,
)
],
),
),
],
),
);
}

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) {
url_launcher.launchUrl(Uri.parse(href));
parlough marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
14 changes: 8 additions & 6 deletions pkgs/sketch_pad/lib/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,8 @@ import 'package:web/web.dart' as web;
import '../model.dart';
import 'codemirror.dart';

// TODO: show documentation on hover

// TODO: implement find / find next

// TODO: improve the code completion UI

// TODO: hover - show links to hosted dartdoc? (flutter, dart api, packages)

const String _viewType = 'dartpad-editor';

bool _viewFactoryInitialized = false;
Expand Down Expand Up @@ -228,6 +222,14 @@ 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
97 changes: 83 additions & 14 deletions pkgs/sketch_pad/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import 'package:url_launcher/url_launcher.dart' as url_launcher;
import 'package:vtable/vtable.dart';

import 'console.dart';
import 'docs.dart';
import 'editor/editor.dart';
import 'embed.dart';
import 'execution/execution.dart';
Expand Down Expand Up @@ -229,12 +230,11 @@ 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');

late final VoidCallback runStartedListener;

@override
void initState() {
super.initState();
Expand All @@ -247,14 +247,6 @@ class _DartPadMainPageState extends State<DartPadMainPage>
setState(() {});
},
);
runStartedListener = () {
setState(() {
// Switch to the application output tab.]
if (appModel.compilingBusy.value) {
tabController.animateTo(1);
}
});
};

final leftPanelSize = widget.embedMode ? 0.62 : 0.50;
mainSplitter =
Expand Down Expand Up @@ -290,12 +282,14 @@ class _DartPadMainPageState extends State<DartPadMainPage>
}
});

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

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

appServices.dispose();
appModel.dispose();
Expand Down Expand Up @@ -326,6 +320,32 @@ 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 @@ -390,7 +410,7 @@ class _DartPadMainPageState extends State<DartPadMainPage>
child: IndexedStack(
index: tabController.index,
children: [
editor,
editingGroup,
executionStack,
],
),
Expand Down Expand Up @@ -422,7 +442,7 @@ class _DartPadMainPageState extends State<DartPadMainPage>
gripSize: defaultGripSize,
controller: mainSplitter,
children: [
editor,
editingGroup,
executionStack,
],
),
Expand Down Expand Up @@ -535,6 +555,55 @@ class _DartPadMainPageState extends State<DartPadMainPage>
progress.close();
}
}

void _handleRunStarted() {
setState(() {
// Switch to the application output tab.]
if (appModel.compilingBusy.value) {
tabController.animateTo(1);
}
});
}

static final RegExp identifierChar = RegExp(r'[\w\d_<=>]');

void _handleDocClicked() async {
// TODO: Support having the escape key close the doc panel.

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
21 changes: 17 additions & 4 deletions pkgs/sketch_pad/lib/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ class AppModel {
final ValueNotifier<LayoutMode> _layoutMode = ValueNotifier(LayoutMode.both);
ValueListenable<LayoutMode> get layoutMode => _layoutMode;

/// Whether the docs panel is showing or should show.
final ValueNotifier<bool> docsShowing = ValueNotifier(false);

/// The last document request received.
final ValueNotifier<DocumentResponse?> currentDocs = ValueNotifier(null);

/// Used to pass information about mouse clicks in the editor.
final ValueNotifier<int> lastEditorClickOffset = ValueNotifier(0);

final ValueNotifier<SplitDragState> splitViewDragState =
ValueNotifier(SplitDragState.inactive);

Expand Down Expand Up @@ -103,13 +112,13 @@ class AppModel {
}
}

const double dividerSplit = 0.78;

enum LayoutMode {
both(true, true),
justDom(true, false),
justConsole(false, true);

static const double _dividerSplit = 0.78;

final bool domIsVisible;
final bool consoleIsVisible;

Expand All @@ -119,14 +128,14 @@ enum LayoutMode {
if (!domIsVisible) return 1;
if (!consoleIsVisible) return height;

return height * _dividerSplit;
return height * dividerSplit;
}

double calcConsoleHeight(double height) {
if (!consoleIsVisible) return 0;
if (!domIsVisible) return height - 1;

return height * (1 - _dividerSplit);
return height * (1 - dividerSplit);
}
}

Expand Down Expand Up @@ -304,6 +313,10 @@ class AppServices {
}
}

Future<DocumentResponse> document(SourceRequest request) async {
return await services.document(request);
}

Future<CompileResponse> compile(CompileRequest request) async {
try {
appModel.compilingBusy.value = true;
Expand Down
1 change: 1 addition & 0 deletions pkgs/sketch_pad/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies:
dartpad_shared: any
flutter:
sdk: flutter
flutter_markdown: ^0.6.22
flutter_web_plugins:
sdk: flutter
fluttering_phrases: ^1.0.0
Expand Down
Loading