Skip to content

Commit

Permalink
Add more workshop features (#1834)
Browse files Browse the repository at this point in the history
* DartPad instructor UI phase 2

- analysis results
- completions
- doc panel
- Hide UI Output in Dart mode
- busy light
- keyboard shortcuts
- format button

* remove unused import
  • Loading branch information
johnpryan committed Apr 27, 2021
1 parent 8642486 commit 5c16db3
Show file tree
Hide file tree
Showing 9 changed files with 349 additions and 51 deletions.
251 changes: 249 additions & 2 deletions lib/codelab.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import 'dart:async';
import 'dart:html' hide Console;

import 'package:dart_pad/context.dart';
import 'package:dart_pad/util/query_params.dart';
import 'package:logging/logging.dart';
import 'package:markdown/markdown.dart' as markdown;
import 'package:mdc_web/mdc_web.dart';
import 'package:split/split.dart';
import 'package:stream_transform/stream_transform.dart';

import 'codelabs/codelabs.dart';
import 'completion.dart';
import 'core/dependencies.dart';
import 'core/modules.dart';
import 'dart_pad.dart';
import 'documentation.dart';
import 'editing/codemirror_options.dart';
import 'editing/editor.dart';
import 'editing/editor_codemirror.dart';
import 'elements/analysis_results_controller.dart';
import 'elements/button.dart';
import 'elements/console.dart';
import 'elements/counter.dart';
Expand All @@ -28,11 +34,14 @@ import 'services/execution.dart';
import 'services/execution_iframe.dart';
import 'src/ga.dart';
import 'hljs.dart' as hljs;
import 'util/keymap.dart';

CodelabUi _codelabUi;

CodelabUi get codelabUi => _codelabUi;

final Logger _logger = Logger('dartpad');

void init() {
_codelabUi = CodelabUi();
}
Expand All @@ -51,6 +60,12 @@ class CodelabUi {
MaterialTabController consolePanelTabController;
Counter unreadConsoleCounter;
Dialog dialog;
DocHandler docHandler;
Future _analysisRequest;
DartSourceProvider sourceProvider;
MDCButton formatButton;
DBusyLight busyLight;
AnalysisResultsController analysisResultsController;

CodelabUi() {
_init();
Expand All @@ -69,10 +84,12 @@ class CodelabUi {
Future<void> _init() async {
_initDialogs();
await _loadCodelab();
_initBusyLights();
_initHeader();
_updateInstructions();
await _initModules();
_initCodelabUi();
_initKeyBindings();
_initEditor();
_initSplitters();
_initStepButtons();
Expand All @@ -81,6 +98,7 @@ class CodelabUi {
_initButtons();
_updateCode();
_initTabs();
_focusEditor();
}

Future<void> _initModules() async {
Expand All @@ -97,13 +115,38 @@ class CodelabUi {
dialog = Dialog();
}

void _initBusyLights() {
busyLight = DBusyLight(querySelector('#dartbusy'));
}

void _initEditor() {
// Set up CodeMirror
editor = (editorFactory as CodeMirrorFactory)
.createFromElement(_editorHost, options: codeMirrorOptions)
..theme = 'darkpad'
..mode = 'dart'
..showLineNumbers = true;

sourceProvider = CodelabDartSourceProvider(editor);
docHandler = DocHandler(editor, sourceProvider);

editor.document.onChange.listen((_) => busyLight.on());
editor.document.onChange
.debounce(Duration(milliseconds: 1250))
.listen((_) => _performAnalysis());

editorFactory.registerCompleter(
'dart', DartCompleter(dartServices, editor.document));

// Listen for changes that would effect the documentation panel.
editor.onMouseDown.listen((e) {
// Delay to give codemirror time to process the mouse event.
Timer.run(() {
if (!_cursorPositionIsWhitespace()) {
docHandler.generateDoc([_documentationElement]);
}
});
});
}

void _initCodelabUi() {
Expand All @@ -116,6 +159,69 @@ class CodelabUi {

// Use null safety for codelabs
(deps[DartservicesApi] as DartservicesApi).rootUrl = nullSafetyServerUrl;

analysisResultsController = AnalysisResultsController(
DElement(querySelector('#issues')),
DElement(querySelector('#issues-message')),
DElement(querySelector('#issues-toggle')))
..onItemClicked.listen((item) {
_jumpTo(item.line, item.charStart, item.charLength, focus: true);
});

_updateVersion();

querySelector('#keyboard-button')
.onClick
.listen((_) => _showKeyboardDialog());
}

void _initKeyBindings() {
// set up key bindings
keys.bind(['ctrl-enter'], _handleRun, 'Run');
keys.bind(['f1'], () {
ga.sendEvent('main', 'help');
docHandler.generateDoc([_documentationElement]);
}, 'Documentation');

keys.bind(['alt-enter'], () {
editor.showCompletions(onlyShowFixes: true);
}, 'Quick fix');

keys.bind(['ctrl-space', 'macctrl-space'], () {
editor.showCompletions();
}, 'Completion');

keys.bind(['shift-ctrl-/', 'shift-macctrl-/'], () {
_showKeyboardDialog();
}, 'Keyboard Shortcuts');
keys.bind(['shift-ctrl-f', 'shift-macctrl-f'], () {
_format();
}, 'Format');

document.onKeyUp.listen((e) {
if (editor.completionActive ||
DocHandler.cursorKeys.contains(e.keyCode)) {
docHandler.generateDoc([_documentationElement]);
}
_handleAutoCompletion(e);
});
}

void _handleAutoCompletion(KeyboardEvent e) {
if (editor.hasFocus) {
if (e.keyCode == KeyCode.PERIOD) {
editor.showCompletions(autoInvoked: true);
}
}
}

void _updateVersion() {
dartServices.version().then((VersionResponse version) {
// "Based on Flutter 1.19.0-4.1.pre Dart SDK 2.8.4"
var versionText = 'Based on Flutter ${version.flutterVersion}'
' Dart SDK ${version.sdkVersionFull}';
querySelector('#dartpad-version').text = versionText;
}).catchError((e) => null);
}

Future<void> _loadCodelab() async {
Expand Down Expand Up @@ -189,6 +295,8 @@ class CodelabUi {
showSolutionButton =
MDCButton(querySelector('#show-solution-btn') as ButtonElement)
..onClick.listen((_) => _handleShowSolution());
formatButton = MDCButton(querySelector('#format-button') as ButtonElement)
..onClick.listen((_) => _format());
}

void _updateSolutionButton() {
Expand All @@ -213,8 +321,15 @@ class CodelabUi {
_changeConsoleTab(name);
}));
}
// Set the current tab to UI Output
_changeConsoleTab('ui-output');

// Set the current tab to UI Output or console, depending on whether this is
// Dart or Flutter codelab.
if (_codelabState.codelab.type == CodelabType.dart) {
querySelector('#ui-output-tab').hidden = true;
consolePanelTabController.selectTab('console');
} else {
consolePanelTabController.selectTab('ui-output');
}
}

void _changeConsoleTab(String name) {
Expand Down Expand Up @@ -272,6 +387,10 @@ class CodelabUi {
'"gh_owner", "gh_repo", "gh_ref", and "gh_path"');
}

void _showKeyboardDialog() {
dialog.showOk('Keyboard shortcuts', keyMapToHtml(keys.inverseBindings));
}

void _handleRun() async {
ga.sendEvent('main', 'run');
runButton.disabled = true;
Expand Down Expand Up @@ -330,6 +449,99 @@ class CodelabUi {
}
}

/// Perform static analysis of the source code. Return whether the code
/// analyzed cleanly (had no errors or warnings).
Future<bool> _performAnalysis() {
var input = SourceRequest()..source = sourceProvider.dartSource;

var lines = Lines(input.source);

var request = dartServices.analyze(input).timeout(serviceCallTimeout);
_analysisRequest = request;

return request.then((AnalysisResults result) {
// Discard if we requested another analysis.
if (_analysisRequest != request) return false;

// Discard if the document has been mutated since we requested analysis.
if (input.source != sourceProvider.dartSource) return false;

busyLight.reset();

_displayIssues(result.issues);

editor.document.setAnnotations(result.issues.map((AnalysisIssue issue) {
var startLine = lines.getLineForOffset(issue.charStart);
var endLine =
lines.getLineForOffset(issue.charStart + issue.charLength);

var start = Position(
startLine, issue.charStart - lines.offsetForLine(startLine));
var end = Position(
endLine,
issue.charStart +
issue.charLength -
lines.offsetForLine(startLine));

return Annotation(issue.kind, issue.message, issue.line,
start: start, end: end);
}).toList());

var hasErrors = result.issues.any((issue) => issue.kind == 'error');
var hasWarnings = result.issues.any((issue) => issue.kind == 'warning');

return hasErrors == false && hasWarnings == false;
}).catchError((e) {
if (e is! TimeoutException) {
final message = e is ApiRequestError ? e.message : '$e';

_displayIssues([
AnalysisIssue()
..kind = 'error'
..line = 1
..message = message
]);
} else {
_logger.severe(e);
}

editor.document.setAnnotations([]);
busyLight.reset();
});
}

Future<void> _format() {
var originalSource = sourceProvider.dartSource;
var input = SourceRequest()..source = originalSource;
formatButton.disabled = true;

var request = dartServices.format(input).timeout(serviceCallTimeout);
return request.then((FormatResponse result) {
busyLight.reset();
formatButton.disabled = false;

if (result.newString == null || result.newString.isEmpty) {
_logger.fine('Format returned null/empty result');
return;
}

if (originalSource != result.newString) {
editor.document.updateValue(result.newString);
_showSnackbar('Format successful.');
} else {
_showSnackbar('No formatting changes.');
}
}).catchError((e) {
busyLight.reset();
formatButton.disabled = false;
_logger.severe(e);
});
}

void _focusEditor() {
editor.focus();
}

void _clearOutput() {
_console.clear();
unreadConsoleCounter.clear();
Expand Down Expand Up @@ -358,6 +570,29 @@ class CodelabUi {
showSolutionButton.disabled = true;
}
}

void _displayIssues(List<AnalysisIssue> issues) {
analysisResultsController.display(issues);
}

void _jumpTo(int line, int charStart, int charLength, {bool focus = false}) {
final doc = editor.document;

doc.select(
doc.posFromIndex(charStart), doc.posFromIndex(charStart + charLength));

if (focus) editor.focus();
}

/// Return true if the current cursor position is in a whitespace char.
bool _cursorPositionIsWhitespace() {
var document = editor.document;
var str = document.value;
var index = document.indexFromPos(document.cursor);
if (index < 0 || index >= str.length) return false;
var char = str[index];
return char != char.trim();
}
}

class CodelabState {
Expand Down Expand Up @@ -386,3 +621,15 @@ class CodelabState {

bool get hasPreviousStep => _currentStepIndex > 0;
}

class CodelabDartSourceProvider implements DartSourceProvider {
final Editor editor;

CodelabDartSourceProvider(this.editor);

@override
String get dartSource => editor.document.value;

@override
bool get isFocused => true;
}
10 changes: 9 additions & 1 deletion lib/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ library context;

import 'services/dartservices.dart';

abstract class Context {
abstract class DartSourceProvider {
bool get isFocused;
String get dartSource;
}

abstract class Context implements DartSourceProvider {
final List<AnalysisIssue> issues = [];

String get focusedEditor;
@override
bool get isFocused => focusedEditor == 'dart';

String name;
String description;

@override
String dartSource;
String htmlSource;
String cssSource;
Expand Down
Loading

0 comments on commit 5c16db3

Please sign in to comment.