| Index: site/try/src/interaction_manager.dart
|
| diff --git a/site/try/src/interaction_manager.dart b/site/try/src/interaction_manager.dart
|
| deleted file mode 100644
|
| index 89ddf7605b32f28c3c049193277cb0a1269e7a2f..0000000000000000000000000000000000000000
|
| --- a/site/try/src/interaction_manager.dart
|
| +++ /dev/null
|
| @@ -1,1356 +0,0 @@
|
| -// Copyright (c) 2014, 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.
|
| -
|
| -library trydart.interaction_manager;
|
| -
|
| -import 'dart:html';
|
| -
|
| -import 'dart:convert' show
|
| - JSON;
|
| -
|
| -import 'dart:math' show
|
| - max,
|
| - min;
|
| -
|
| -import 'dart:async' show
|
| - Completer,
|
| - Future,
|
| - Timer;
|
| -
|
| -import 'dart:collection' show
|
| - Queue;
|
| -
|
| -import 'package:compiler/src/scanner/string_scanner.dart' show
|
| - StringScanner;
|
| -
|
| -import 'package:compiler/src/tokens/token.dart' show
|
| - BeginGroupToken,
|
| - ErrorToken,
|
| - Token,
|
| - UnmatchedToken,
|
| - UnterminatedToken;
|
| -
|
| -import 'package:compiler/src/tokens/token_constants.dart' show
|
| - EOF_TOKEN,
|
| - STRING_INTERPOLATION_IDENTIFIER_TOKEN,
|
| - STRING_INTERPOLATION_TOKEN,
|
| - STRING_TOKEN;
|
| -
|
| -import 'package:compiler/src/io/source_file.dart' show
|
| - StringSourceFile;
|
| -
|
| -import 'package:compiler/src/string_validator.dart' show
|
| - StringValidator;
|
| -
|
| -import 'package:compiler/src/tree/tree.dart' show
|
| - StringQuoting;
|
| -
|
| -import 'compilation.dart' show
|
| - currentSource,
|
| - startCompilation;
|
| -
|
| -import 'ui.dart' show
|
| - currentTheme,
|
| - hackDiv,
|
| - mainEditorPane,
|
| - observer,
|
| - outputDiv,
|
| - outputFrame,
|
| - statusDiv;
|
| -
|
| -import 'decoration.dart' show
|
| - CodeCompletionDecoration,
|
| - Decoration,
|
| - DiagnosticDecoration,
|
| - error,
|
| - info,
|
| - warning;
|
| -
|
| -import 'html_to_text.dart' show
|
| - htmlToText;
|
| -
|
| -import 'compilation_unit.dart' show
|
| - CompilationUnit;
|
| -
|
| -import 'selection.dart' show
|
| - TrySelection,
|
| - isCollapsed;
|
| -
|
| -import 'editor.dart' as editor;
|
| -
|
| -import 'mock.dart' as mock;
|
| -
|
| -import 'settings.dart' as settings;
|
| -
|
| -import 'shadow_root.dart' show
|
| - getShadowRoot,
|
| - getText,
|
| - setShadowRoot,
|
| - containsNode;
|
| -
|
| -import 'iframe_error_handler.dart' show
|
| - ErrorMessage;
|
| -
|
| -const String TRY_DART_NEW_DEFECT =
|
| - 'https://code.google.com/p/dart/issues/entry'
|
| - '?template=Try+Dart+Internal+Error';
|
| -
|
| -/// How frequently [InteractionManager.onHeartbeat] is called.
|
| -const Duration HEARTBEAT_INTERVAL = const Duration(milliseconds: 50);
|
| -
|
| -/// Determines how frequently "project" files are saved. The time is measured
|
| -/// from the time of last modification.
|
| -const Duration SAVE_INTERVAL = const Duration(seconds: 5);
|
| -
|
| -/// Determines how frequently the compiler is invoked. The time is measured
|
| -/// from the time of last modification.
|
| -const Duration COMPILE_INTERVAL = const Duration(seconds: 1);
|
| -
|
| -/// Determines how frequently the compiler is invoked in "live" mode. The time
|
| -/// is measured from the time of last modification.
|
| -const Duration LIVE_COMPILE_INTERVAL = const Duration(seconds: 0);
|
| -
|
| -/// Determines if a compilation is slow. The time is measured from the last
|
| -/// compilation started. If a compilation is slow, progress information is
|
| -/// displayed to the user, but the console is untouched if the compilation
|
| -/// finished quickly. The purpose is to reduce flicker in the UI.
|
| -const Duration SLOW_COMPILE = const Duration(seconds: 1);
|
| -
|
| -const int TAB_WIDTH = 2;
|
| -
|
| -/**
|
| - * UI interaction manager for the entire application.
|
| - */
|
| -abstract class InteractionManager {
|
| - // Design note: All UI interactions go through one instance of this
|
| - // class. This is by design.
|
| - //
|
| - // Simplicity in UI is in the eye of the beholder, not the implementor. Great
|
| - // 'natural UI' is usually achieved with substantial implementation
|
| - // complexity that doesn't modularize well and has nasty complicated state
|
| - // dependencies.
|
| - //
|
| - // In rare cases, some UI components can be independent of this state
|
| - // machine. For example, animation and auto-save loops.
|
| -
|
| - // Implementation note: The state machine is actually implemented by
|
| - // [InteractionContext], this class represents public event handlers.
|
| -
|
| - factory InteractionManager() => new InteractionContext();
|
| -
|
| - InteractionManager.internal();
|
| -
|
| - // TODO(ahe): Remove this.
|
| - Set<AnchorElement> get oldDiagnostics;
|
| -
|
| - void onInput(Event event);
|
| -
|
| - // TODO(ahe): Rename to onKeyDown (as it is called in response to keydown
|
| - // event).
|
| - void onKeyUp(KeyboardEvent event);
|
| -
|
| - void onMutation(List<MutationRecord> mutations, MutationObserver observer);
|
| -
|
| - void onSelectionChange(Event event);
|
| -
|
| - /// Called when the content of a CompilationUnit changed.
|
| - void onCompilationUnitChanged(CompilationUnit unit);
|
| -
|
| - Future<List<String>> projectFileNames();
|
| -
|
| - /// Called when the user selected a new project file.
|
| - void onProjectFileSelected(String projectFile);
|
| -
|
| - /// Called when notified about a project file changed (on the server).
|
| - void onProjectFileFsEvent(MessageEvent e);
|
| -
|
| - /// Called every [HEARTBEAT_INTERVAL].
|
| - void onHeartbeat(Timer timer);
|
| -
|
| - /// Called by [:window.onMessage.listen:].
|
| - void onWindowMessage(MessageEvent event);
|
| -
|
| - void onCompilationFailed(String firstError);
|
| -
|
| - void onCompilationDone();
|
| -
|
| - /// Called when a compilation is starting, but just before sending the
|
| - /// initiating message to the compiler isolate.
|
| - void compilationStarting();
|
| -
|
| - // TODO(ahe): Remove this from InteractionManager, but not from InitialState.
|
| - void consolePrintLine(line);
|
| -
|
| - /// Called just before running a freshly compiled program.
|
| - void aboutToRun();
|
| -
|
| - /// Called when an error occurs when running user code in an iframe.
|
| - void onIframeError(ErrorMessage message);
|
| -
|
| - void verboseCompilerMessage(String message);
|
| -
|
| - /// Called if the compiler crashes.
|
| - void onCompilerCrash(data);
|
| -
|
| - /// Called if an internal error is detected.
|
| - void onInternalError(message);
|
| -}
|
| -
|
| -/**
|
| - * State machine for UI interactions.
|
| - */
|
| -class InteractionContext extends InteractionManager {
|
| - InteractionState state;
|
| -
|
| - final Map<String, CompilationUnit> projectFiles = <String, CompilationUnit>{};
|
| -
|
| - final Set<CompilationUnit> modifiedUnits = new Set<CompilationUnit>();
|
| -
|
| - final Queue<CompilationUnit> unitsToSave = new Queue<CompilationUnit>();
|
| -
|
| - /// Tracks time since last modification of a "project" file.
|
| - final Stopwatch saveTimer = new Stopwatch();
|
| -
|
| - /// Tracks time since last modification.
|
| - final Stopwatch compileTimer = new Stopwatch();
|
| -
|
| - /// Tracks elapsed time of current compilation.
|
| - final Stopwatch elapsedCompilationTime = new Stopwatch();
|
| -
|
| - CompilationUnit currentCompilationUnit =
|
| - // TODO(ahe): Don't use a fake unit.
|
| - new CompilationUnit('fake', '');
|
| -
|
| - Timer heartbeat;
|
| -
|
| - Completer<String> completeSaveOperation;
|
| -
|
| - bool shouldClearConsole = false;
|
| -
|
| - Element compilerConsole;
|
| -
|
| - bool isFirstCompile = true;
|
| -
|
| - final Set<AnchorElement> oldDiagnostics = new Set<AnchorElement>();
|
| -
|
| - final Duration compileInterval = settings.live.value
|
| - ? LIVE_COMPILE_INTERVAL
|
| - : COMPILE_INTERVAL;
|
| -
|
| - InteractionContext()
|
| - : super.internal() {
|
| - state = new InitialState(this);
|
| - heartbeat = new Timer.periodic(HEARTBEAT_INTERVAL, onHeartbeat);
|
| - }
|
| -
|
| - void onInput(Event event) => state.onInput(event);
|
| -
|
| - void onKeyUp(KeyboardEvent event) => state.onKeyUp(event);
|
| -
|
| - void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| - workAroundFirefoxBug();
|
| - try {
|
| - try {
|
| - return state.onMutation(mutations, observer);
|
| - } finally {
|
| - // Discard any mutations during the observer, as these can lead to
|
| - // infinite loop.
|
| - observer.takeRecords();
|
| - }
|
| - } catch (error, stackTrace) {
|
| - try {
|
| - editor.isMalformedInput = true;
|
| - state.onInternalError(
|
| - '\nError and stack trace:\n$error\n$stackTrace\n');
|
| - } catch (e) {
|
| - // Double faults ignored.
|
| - }
|
| - rethrow;
|
| - }
|
| - }
|
| -
|
| - void onSelectionChange(Event event) => state.onSelectionChange(event);
|
| -
|
| - void onCompilationUnitChanged(CompilationUnit unit) {
|
| - return state.onCompilationUnitChanged(unit);
|
| - }
|
| -
|
| - Future<List<String>> projectFileNames() => state.projectFileNames();
|
| -
|
| - void onProjectFileSelected(String projectFile) {
|
| - return state.onProjectFileSelected(projectFile);
|
| - }
|
| -
|
| - void onProjectFileFsEvent(MessageEvent e) {
|
| - return state.onProjectFileFsEvent(e);
|
| - }
|
| -
|
| - void onHeartbeat(Timer timer) => state.onHeartbeat(timer);
|
| -
|
| - void onWindowMessage(MessageEvent event) => state.onWindowMessage(event);
|
| -
|
| - void onCompilationFailed(String firstError) {
|
| - return state.onCompilationFailed(firstError);
|
| - }
|
| -
|
| - void onCompilationDone() => state.onCompilationDone();
|
| -
|
| - void compilationStarting() => state.compilationStarting();
|
| -
|
| - void consolePrintLine(line) => state.consolePrintLine(line);
|
| -
|
| - void aboutToRun() => state.aboutToRun();
|
| -
|
| - void onIframeError(ErrorMessage message) => state.onIframeError(message);
|
| -
|
| - void verboseCompilerMessage(String message) {
|
| - return state.verboseCompilerMessage(message);
|
| - }
|
| -
|
| - void onCompilerCrash(data) => state.onCompilerCrash(data);
|
| -
|
| - void onInternalError(message) => state.onInternalError(message);
|
| -}
|
| -
|
| -abstract class InteractionState implements InteractionManager {
|
| - InteractionContext get context;
|
| -
|
| - // TODO(ahe): Remove this.
|
| - Set<AnchorElement> get oldDiagnostics {
|
| - throw 'Use context.oldDiagnostics instead';
|
| - }
|
| -
|
| - void set state(InteractionState newState);
|
| -
|
| - void onStateChanged(InteractionState previous) {
|
| - }
|
| -
|
| - void transitionToInitialState() {
|
| - state = new InitialState(context);
|
| - }
|
| -}
|
| -
|
| -class InitialState extends InteractionState {
|
| - final InteractionContext context;
|
| - bool requestCodeCompletion = false;
|
| -
|
| - InitialState(this.context);
|
| -
|
| - void set state(InteractionState state) {
|
| - InteractionState previous = context.state;
|
| - if (previous != state) {
|
| - context.state = state;
|
| - state.onStateChanged(previous);
|
| - }
|
| - }
|
| -
|
| - void onInput(Event event) {
|
| - state = new PendingInputState(context);
|
| - }
|
| -
|
| - void onKeyUp(KeyboardEvent event) {
|
| - if (computeHasModifier(event)) {
|
| - onModifiedKeyUp(event);
|
| - } else {
|
| - onUnmodifiedKeyUp(event);
|
| - }
|
| - }
|
| -
|
| - void onModifiedKeyUp(KeyboardEvent event) {
|
| - if (event.getModifierState("Shift")) return onShiftedKeyUp(event);
|
| - switch (event.keyCode) {
|
| - case KeyCode.S:
|
| - // Disable Ctrl-S, Cmd-S, etc. We have observed users hitting these
|
| - // keys often when using Try Dart and getting frustrated.
|
| - event.preventDefault();
|
| - // TODO(ahe): Consider starting a compilation.
|
| - break;
|
| - }
|
| - }
|
| -
|
| - void onShiftedKeyUp(KeyboardEvent event) {
|
| - switch (event.keyCode) {
|
| - case KeyCode.TAB:
|
| - event.preventDefault();
|
| - break;
|
| - }
|
| - }
|
| -
|
| - void onUnmodifiedKeyUp(KeyboardEvent event) {
|
| - switch (event.keyCode) {
|
| - case KeyCode.ENTER: {
|
| - Selection selection = window.getSelection();
|
| - if (isCollapsed(selection)) {
|
| - event.preventDefault();
|
| - Node node = selection.anchorNode;
|
| - if (node is Text) {
|
| - Text text = node;
|
| - int offset = selection.anchorOffset;
|
| - // If at end-of-file, insert an extra newline. The the extra
|
| - // newline ensures that the next line isn't empty. At least Chrome
|
| - // behaves as if "\n" is just a single line. "\nc" (where c is any
|
| - // character) is two lines, according to Chrome.
|
| - String newline = isAtEndOfFile(text, offset) ? '\n\n' : '\n';
|
| - text.insertData(offset, newline);
|
| - selection.collapse(text, offset + 1);
|
| - } else if (node is Element) {
|
| - node.appendText('\n\n');
|
| - selection.collapse(node.firstChild, 1);
|
| - } else {
|
| - window.console
|
| - ..error('Unexpected node')
|
| - ..dir(node);
|
| - }
|
| - }
|
| - break;
|
| - }
|
| - case KeyCode.TAB: {
|
| - Selection selection = window.getSelection();
|
| - if (isCollapsed(selection)) {
|
| - event.preventDefault();
|
| - Text text = new Text(' ' * TAB_WIDTH);
|
| - selection.getRangeAt(0).insertNode(text);
|
| - selection.collapse(text, TAB_WIDTH);
|
| - }
|
| - break;
|
| - }
|
| - }
|
| -
|
| - // This is a hack to get Safari (iOS) to send mutation events on
|
| - // contenteditable.
|
| - // TODO(ahe): Move to onInput?
|
| - var newDiv = new DivElement();
|
| - hackDiv.replaceWith(newDiv);
|
| - hackDiv = newDiv;
|
| - }
|
| -
|
| - void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| - removeCodeCompletion();
|
| -
|
| - Selection selection = window.getSelection();
|
| - TrySelection trySelection = new TrySelection(mainEditorPane, selection);
|
| -
|
| - Set<Node> normalizedNodes = new Set<Node>();
|
| - for (MutationRecord record in mutations) {
|
| - normalizeMutationRecord(record, trySelection, normalizedNodes);
|
| - }
|
| -
|
| - if (normalizedNodes.length == 1) {
|
| - Node node = normalizedNodes.single;
|
| - if (node is Element && node.classes.contains('lineNumber')) {
|
| - print('Single line change: ${node.outerHtml}');
|
| -
|
| - updateHighlighting(node, selection, trySelection, mainEditorPane);
|
| - return;
|
| - }
|
| - }
|
| -
|
| - updateHighlighting(mainEditorPane, selection, trySelection);
|
| - }
|
| -
|
| - void updateHighlighting(
|
| - Element node,
|
| - Selection selection,
|
| - TrySelection trySelection,
|
| - [Element root]) {
|
| - String state = '';
|
| - String currentText = getText(node);
|
| - if (root != null) {
|
| - // Single line change.
|
| - trySelection = trySelection.copyWithRoot(node);
|
| - Element previousLine = node.previousElementSibling;
|
| - if (previousLine != null) {
|
| - state = previousLine.getAttribute('dart-state');
|
| - }
|
| -
|
| - node.parentNode.insertAllBefore(
|
| - createHighlightedNodes(trySelection, currentText, state),
|
| - node);
|
| - node.remove();
|
| - } else {
|
| - root = node;
|
| - editor.seenIdentifiers = new Set<String>.from(mock.identifiers);
|
| -
|
| - // Fail safe: new [nodes] are computed before clearing old nodes.
|
| - List<Node> nodes =
|
| - createHighlightedNodes(trySelection, currentText, state);
|
| -
|
| - node.nodes
|
| - ..clear()
|
| - ..addAll(nodes);
|
| - }
|
| -
|
| - if (containsNode(mainEditorPane, trySelection.anchorNode)) {
|
| - // Sometimes the anchor node is removed by the above call. This has
|
| - // only been observed in Firefox, and is hard to reproduce.
|
| - trySelection.adjust(selection);
|
| - }
|
| -
|
| - // TODO(ahe): We know almost exactly what has changed. It could be
|
| - // more efficient to only communicate what changed.
|
| - context.currentCompilationUnit.content = getText(root);
|
| -
|
| - // Discard highlighting mutations.
|
| - observer.takeRecords();
|
| - }
|
| -
|
| - List<Node> createHighlightedNodes(
|
| - TrySelection trySelection,
|
| - String currentText,
|
| - String state) {
|
| - trySelection.updateText(currentText);
|
| -
|
| - editor.isMalformedInput = false;
|
| - int offset = 0;
|
| - List<Node> nodes = <Node>[];
|
| -
|
| - for (String line in splitLines(currentText)) {
|
| - List<Node> lineNodes = <Node>[];
|
| - state =
|
| - tokenizeAndHighlight(line, state, offset, trySelection, lineNodes);
|
| - offset += line.length;
|
| - nodes.add(makeLine(lineNodes, state));
|
| - }
|
| -
|
| - return nodes;
|
| - }
|
| -
|
| - void onSelectionChange(Event event) {
|
| - }
|
| -
|
| - void onStateChanged(InteractionState previous) {
|
| - super.onStateChanged(previous);
|
| - context.compileTimer
|
| - ..start()
|
| - ..reset();
|
| - }
|
| -
|
| - void onCompilationUnitChanged(CompilationUnit unit) {
|
| - if (unit == context.currentCompilationUnit) {
|
| - currentSource = unit.content;
|
| - if (context.projectFiles.containsKey(unit.name)) {
|
| - postProjectFileUpdate(unit);
|
| - }
|
| - context.compileTimer.start();
|
| - } else {
|
| - print("Unexpected change to compilation unit '${unit.name}'.");
|
| - }
|
| - }
|
| -
|
| - void postProjectFileUpdate(CompilationUnit unit) {
|
| - context.modifiedUnits.add(unit);
|
| - context.saveTimer.start();
|
| - }
|
| -
|
| - Future<List<String>> projectFileNames() {
|
| - return getString('project?list').then((String response) {
|
| - WebSocket socket = new WebSocket('ws://127.0.0.1:9090/ws/watch');
|
| - socket.onMessage.listen(context.onProjectFileFsEvent);
|
| - return new List<String>.from(JSON.decode(response));
|
| - });
|
| - }
|
| -
|
| - void onProjectFileSelected(String projectFile) {
|
| - // Disable editing whilst fetching data.
|
| - mainEditorPane.contentEditable = 'false';
|
| -
|
| - CompilationUnit unit = context.projectFiles[projectFile];
|
| - Future<CompilationUnit> future;
|
| - if (unit != null) {
|
| - // This project file had been fetched already.
|
| - future = new Future<CompilationUnit>.value(unit);
|
| -
|
| - // TODO(ahe): Probably better to fetch the sources again.
|
| - } else {
|
| - // This project file has to be fetched.
|
| - future = getString('project/$projectFile').then((String text) {
|
| - CompilationUnit unit = context.projectFiles[projectFile];
|
| - if (unit == null) {
|
| - // Only create a new unit if the value hadn't arrived already.
|
| - unit = new CompilationUnit(projectFile, text);
|
| - context.projectFiles[projectFile] = unit;
|
| - } else {
|
| - // TODO(ahe): Probably better to overwrite sources. Create a new
|
| - // unit?
|
| - // The server should push updates to the client.
|
| - }
|
| - return unit;
|
| - });
|
| - }
|
| - future.then((CompilationUnit unit) {
|
| - mainEditorPane
|
| - ..contentEditable = 'true'
|
| - ..nodes.clear();
|
| - observer.takeRecords(); // Discard mutations.
|
| -
|
| - transitionToInitialState();
|
| - context.currentCompilationUnit = unit;
|
| -
|
| - // Install the code, which will trigger a call to onMutation.
|
| - mainEditorPane.appendText(unit.content);
|
| - });
|
| - }
|
| -
|
| - void transitionToInitialState() {}
|
| -
|
| - void onProjectFileFsEvent(MessageEvent e) {
|
| - Map map = JSON.decode(e.data);
|
| - List modified = map['modify'];
|
| - if (modified == null) return;
|
| - for (String name in modified) {
|
| - Completer completer = context.completeSaveOperation;
|
| - if (completer != null && !completer.isCompleted) {
|
| - completer.complete(name);
|
| - } else {
|
| - onUnexpectedServerModification(name);
|
| - }
|
| - }
|
| - }
|
| -
|
| - void onUnexpectedServerModification(String name) {
|
| - if (context.currentCompilationUnit.name == name) {
|
| - mainEditorPane.contentEditable = 'false';
|
| - statusDiv.text = 'Modified on disk';
|
| - }
|
| - }
|
| -
|
| - void onHeartbeat(Timer timer) {
|
| - if (context.unitsToSave.isEmpty &&
|
| - context.saveTimer.elapsed > SAVE_INTERVAL) {
|
| - context.saveTimer
|
| - ..stop()
|
| - ..reset();
|
| - context.unitsToSave.addAll(context.modifiedUnits);
|
| - context.modifiedUnits.clear();
|
| - saveUnits();
|
| - }
|
| - if (!settings.compilationPaused &&
|
| - context.compileTimer.elapsed > context.compileInterval) {
|
| - if (startCompilation()) {
|
| - context.compileTimer
|
| - ..stop()
|
| - ..reset();
|
| - }
|
| - }
|
| -
|
| - if (context.elapsedCompilationTime.elapsed > SLOW_COMPILE) {
|
| - if (context.compilerConsole.parent == null) {
|
| - outputDiv.append(context.compilerConsole);
|
| - }
|
| - }
|
| - }
|
| -
|
| - void saveUnits() {
|
| - if (context.unitsToSave.isEmpty) return;
|
| - CompilationUnit unit = context.unitsToSave.removeFirst();
|
| - onError(ProgressEvent event) {
|
| - HttpRequest request = event.target;
|
| - statusDiv.text = "Couldn't save '${unit.name}': ${request.responseText}";
|
| - context.completeSaveOperation.complete(unit.name);
|
| - }
|
| - new HttpRequest()
|
| - ..open("POST", "/project/${unit.name}")
|
| - ..onError.listen(onError)
|
| - ..send(unit.content);
|
| - void setupCompleter() {
|
| - context.completeSaveOperation = new Completer<String>.sync();
|
| - context.completeSaveOperation.future.then((String name) {
|
| - if (name == unit.name) {
|
| - print("Saved source of '$name'");
|
| - saveUnits();
|
| - } else {
|
| - setupCompleter();
|
| - }
|
| - });
|
| - }
|
| - setupCompleter();
|
| - }
|
| -
|
| - void onWindowMessage(MessageEvent event) {
|
| - if (event.source is! WindowBase || event.source == window) {
|
| - return onBadMessage(event);
|
| - }
|
| - if (event.data is List) {
|
| - List message = event.data;
|
| - if (message.length > 0) {
|
| - switch (message[0]) {
|
| - case 'scrollHeight':
|
| - return onScrollHeightMessage(message[1]);
|
| - }
|
| - }
|
| - return onBadMessage(event);
|
| - } else {
|
| - return consolePrintLine(event.data);
|
| - }
|
| - }
|
| -
|
| - /// Called when an iframe is modified.
|
| - void onScrollHeightMessage(int scrollHeight) {
|
| - window.console.log('scrollHeight = $scrollHeight');
|
| - if (scrollHeight > 8) {
|
| - outputFrame.style
|
| - ..height = '${scrollHeight}px'
|
| - ..visibility = ''
|
| - ..position = '';
|
| - while (outputFrame.nextNode is IFrameElement) {
|
| - outputFrame.nextNode.remove();
|
| - }
|
| - }
|
| - }
|
| -
|
| - void onBadMessage(MessageEvent event) {
|
| - window.console
|
| - ..groupCollapsed('Bad message')
|
| - ..dir(event)
|
| - ..log(event.source.runtimeType)
|
| - ..groupEnd();
|
| - }
|
| -
|
| - void consolePrintLine(line) {
|
| - if (context.shouldClearConsole) {
|
| - context.shouldClearConsole = false;
|
| - outputDiv.nodes.clear();
|
| - }
|
| - if (window.parent != window) {
|
| - // Test support.
|
| - // TODO(ahe): Use '/' instead of '*' when Firefox is upgraded to version
|
| - // 30 across build bots. Support for '/' was added in version 29, and we
|
| - // support the two most recent versions.
|
| - window.parent.postMessage('$line\n', '*');
|
| - }
|
| - outputDiv.appendText('$line\n');
|
| - }
|
| -
|
| - void onCompilationFailed(String firstError) {
|
| - if (firstError == null) {
|
| - consolePrintLine('Compilation failed.');
|
| - } else {
|
| - consolePrintLine('Compilation failed: $firstError');
|
| - }
|
| - }
|
| -
|
| - void onCompilationDone() {
|
| - context.isFirstCompile = false;
|
| - context.elapsedCompilationTime.stop();
|
| - Duration compilationDuration = context.elapsedCompilationTime.elapsed;
|
| - context.elapsedCompilationTime.reset();
|
| - print('Compilation took $compilationDuration.');
|
| - if (context.compilerConsole.parent != null) {
|
| - context.compilerConsole.remove();
|
| - }
|
| - for (AnchorElement diagnostic in context.oldDiagnostics) {
|
| - if (diagnostic.parent != null) {
|
| - // Problem fixed, remove the diagnostic.
|
| - diagnostic.replaceWith(new Text(getText(diagnostic)));
|
| - }
|
| - }
|
| - context.oldDiagnostics.clear();
|
| - observer.takeRecords(); // Discard mutations.
|
| - }
|
| -
|
| - void compilationStarting() {
|
| - var progress = new SpanElement()
|
| - ..appendHtml('<i class="icon-spinner icon-spin"></i>')
|
| - ..appendText(' Compiling Dart program.');
|
| - if (settings.verboseCompiler) {
|
| - progress.appendText('..');
|
| - }
|
| - context.compilerConsole = new SpanElement()
|
| - ..append(progress)
|
| - ..appendText('\n');
|
| - context.shouldClearConsole = true;
|
| - context.elapsedCompilationTime
|
| - ..start()
|
| - ..reset();
|
| - if (context.isFirstCompile) {
|
| - outputDiv.append(context.compilerConsole);
|
| - }
|
| - var diagnostics = mainEditorPane.querySelectorAll('a.diagnostic');
|
| - context.oldDiagnostics
|
| - ..clear()
|
| - ..addAll(diagnostics);
|
| - }
|
| -
|
| - void aboutToRun() {
|
| - context.shouldClearConsole = true;
|
| - }
|
| -
|
| - void onIframeError(ErrorMessage message) {
|
| - // TODO(ahe): Consider replacing object URLs with something like <a
|
| - // href='...'>out.js</a>.
|
| - // TODO(ahe): Use source maps to translate stack traces.
|
| - consolePrintLine(message);
|
| - }
|
| -
|
| - void verboseCompilerMessage(String message) {
|
| - if (settings.verboseCompiler) {
|
| - context.compilerConsole.appendText('$message\n');
|
| - } else {
|
| - if (isCompilerStageMarker(message)) {
|
| - Element progress = context.compilerConsole.firstChild;
|
| - progress.appendText('.');
|
| - }
|
| - }
|
| - }
|
| -
|
| - void onCompilerCrash(data) {
|
| - onInternalError('Error and stack trace:\n$data');
|
| - }
|
| -
|
| - void onInternalError(message) {
|
| - outputDiv
|
| - ..nodes.clear()
|
| - ..append(new HeadingElement.h1()..appendText('Internal Error'))
|
| - ..appendText('We would appreciate if you take a moment to report '
|
| - 'this at ')
|
| - ..append(
|
| - new AnchorElement(href: TRY_DART_NEW_DEFECT)
|
| - ..target = '_blank'
|
| - ..appendText(TRY_DART_NEW_DEFECT))
|
| - ..appendText('$message');
|
| - if (window.parent != window) {
|
| - // Test support.
|
| - // TODO(ahe): Use '/' instead of '*' when Firefox is upgraded to version
|
| - // 30 across build bots. Support for '/' was added in version 29, and we
|
| - // support the two most recent versions.
|
| - window.parent.postMessage('$message\n', '*');
|
| - }
|
| - }
|
| -}
|
| -
|
| -Future<String> getString(uri) {
|
| - return new Future<String>.sync(() => HttpRequest.getString('$uri'));
|
| -}
|
| -
|
| -class PendingInputState extends InitialState {
|
| - PendingInputState(InteractionContext context)
|
| - : super(context);
|
| -
|
| - void onInput(Event event) {
|
| - // Do nothing.
|
| - }
|
| -
|
| - void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| - super.onMutation(mutations, observer);
|
| -
|
| - InteractionState nextState = new InitialState(context);
|
| - if (settings.enableCodeCompletion.value) {
|
| - Element parent = editor.getElementAtSelection();
|
| - Element ui;
|
| - if (parent != null) {
|
| - ui = parent.querySelector('.dart-code-completion');
|
| - if (ui != null) {
|
| - nextState = new CodeCompletionState(context, parent, ui);
|
| - }
|
| - }
|
| - }
|
| - state = nextState;
|
| - }
|
| -}
|
| -
|
| -class CodeCompletionState extends InitialState {
|
| - final Element activeCompletion;
|
| - final Element ui;
|
| - int minWidth = 0;
|
| - DivElement staticResults;
|
| - SpanElement inline;
|
| - DivElement serverResults;
|
| - String inlineSuggestion;
|
| -
|
| - CodeCompletionState(InteractionContext context,
|
| - this.activeCompletion,
|
| - this.ui)
|
| - : super(context);
|
| -
|
| - void onInput(Event event) {
|
| - // Do nothing.
|
| - }
|
| -
|
| - void onModifiedKeyUp(KeyboardEvent event) {
|
| - // TODO(ahe): Handle DOWN (jump to server results).
|
| - }
|
| -
|
| - void onUnmodifiedKeyUp(KeyboardEvent event) {
|
| - switch (event.keyCode) {
|
| - case KeyCode.DOWN:
|
| - return moveDown(event);
|
| -
|
| - case KeyCode.UP:
|
| - return moveUp(event);
|
| -
|
| - case KeyCode.ESC:
|
| - event.preventDefault();
|
| - return endCompletion();
|
| -
|
| - case KeyCode.TAB:
|
| - case KeyCode.RIGHT:
|
| - case KeyCode.ENTER:
|
| - event.preventDefault();
|
| - return endCompletion(acceptSuggestion: true);
|
| -
|
| - case KeyCode.SPACE:
|
| - return endCompletion();
|
| - }
|
| - }
|
| -
|
| - void moveDown(Event event) {
|
| - event.preventDefault();
|
| - move(1);
|
| - }
|
| -
|
| - void moveUp(Event event) {
|
| - event.preventDefault();
|
| - move(-1);
|
| - }
|
| -
|
| - void move(int direction) {
|
| - Element element = editor.moveActive(direction, ui);
|
| - if (element == null) return;
|
| - var text = activeCompletion.firstChild;
|
| - String prefix = "";
|
| - if (text is Text) prefix = text.data.trim();
|
| - updateInlineSuggestion(prefix, element.text);
|
| - }
|
| -
|
| - void endCompletion({bool acceptSuggestion: false}) {
|
| - if (acceptSuggestion) {
|
| - suggestionAccepted();
|
| - }
|
| - activeCompletion.classes.remove('active');
|
| - mainEditorPane.querySelectorAll('.hazed-suggestion')
|
| - .forEach((e) => e.remove());
|
| - // The above changes create mutation records. This implicitly fire mutation
|
| - // events that result in saving the source code in local storage.
|
| - // TODO(ahe): Consider making this more explicit.
|
| - state = new InitialState(context);
|
| - }
|
| -
|
| - void suggestionAccepted() {
|
| - if (inlineSuggestion != null) {
|
| - Text text = new Text(inlineSuggestion);
|
| - activeCompletion.replaceWith(text);
|
| - window.getSelection().collapse(text, inlineSuggestion.length);
|
| - }
|
| - }
|
| -
|
| - void onMutation(List<MutationRecord> mutations, MutationObserver observer) {
|
| - for (MutationRecord record in mutations) {
|
| - if (!activeCompletion.contains(record.target)) {
|
| - endCompletion();
|
| - return super.onMutation(mutations, observer);
|
| - }
|
| - }
|
| -
|
| - var text = activeCompletion.firstChild;
|
| - if (text is! Text) return endCompletion();
|
| - updateSuggestions(text.data.trim());
|
| - }
|
| -
|
| - void onStateChanged(InteractionState previous) {
|
| - super.onStateChanged(previous);
|
| - displayCodeCompletion();
|
| - }
|
| -
|
| - void displayCodeCompletion() {
|
| - Selection selection = window.getSelection();
|
| - if (selection.anchorNode is! Text) {
|
| - return endCompletion();
|
| - }
|
| - Text text = selection.anchorNode;
|
| - if (!activeCompletion.contains(text)) {
|
| - return endCompletion();
|
| - }
|
| -
|
| - int anchorOffset = selection.anchorOffset;
|
| -
|
| - String prefix = text.data.substring(0, anchorOffset).trim();
|
| - if (prefix.isEmpty) {
|
| - return endCompletion();
|
| - }
|
| -
|
| - num height = activeCompletion.getBoundingClientRect().height;
|
| - activeCompletion.classes.add('active');
|
| - Node root = getShadowRoot(ui);
|
| -
|
| - inline = new SpanElement()
|
| - ..classes.add('hazed-suggestion');
|
| - Text rest = text.splitText(anchorOffset);
|
| - text.parentNode.insertBefore(inline, text.nextNode);
|
| - activeCompletion.parentNode.insertBefore(
|
| - rest, activeCompletion.nextNode);
|
| -
|
| - staticResults = new DivElement()
|
| - ..classes.addAll(['dart-static', 'dart-limited-height']);
|
| - serverResults = new DivElement()
|
| - ..style.display = 'none'
|
| - ..classes.add('dart-server');
|
| - root.nodes.addAll([staticResults, serverResults]);
|
| - ui.style.top = '${height}px';
|
| -
|
| - staticResults.nodes.add(buildCompletionEntry(prefix));
|
| -
|
| - updateSuggestions(prefix);
|
| - }
|
| -
|
| - void updateInlineSuggestion(String prefix, String suggestion) {
|
| - inlineSuggestion = suggestion;
|
| -
|
| - minWidth = max(minWidth, activeCompletion.getBoundingClientRect().width);
|
| -
|
| - activeCompletion.style
|
| - ..display = 'inline-block'
|
| - ..minWidth = '${minWidth}px';
|
| -
|
| - setShadowRoot(inline, suggestion.substring(prefix.length));
|
| - inline.style.display = '';
|
| -
|
| - observer.takeRecords(); // Discard mutations.
|
| - }
|
| -
|
| - void updateSuggestions(String prefix) {
|
| - if (prefix.isEmpty) {
|
| - return endCompletion();
|
| - }
|
| -
|
| - Token first = tokenize(prefix);
|
| - for (Token token = first; token.kind != EOF_TOKEN; token = token.next) {
|
| - String tokenInfo = token.info.value;
|
| - if (token != first ||
|
| - tokenInfo != 'identifier' &&
|
| - tokenInfo != 'keyword') {
|
| - return endCompletion();
|
| - }
|
| - }
|
| -
|
| - var borderHeight = 2; // 1 pixel border top & bottom.
|
| - num height = ui.getBoundingClientRect().height - borderHeight;
|
| - ui.style.minHeight = '${height}px';
|
| -
|
| - minWidth =
|
| - max(minWidth, activeCompletion.getBoundingClientRect().width);
|
| -
|
| - staticResults.nodes.clear();
|
| - serverResults.nodes.clear();
|
| -
|
| - if (inlineSuggestion != null && inlineSuggestion.startsWith(prefix)) {
|
| - setShadowRoot(inline, inlineSuggestion.substring(prefix.length));
|
| - }
|
| -
|
| - List<String> results = editor.seenIdentifiers.where(
|
| - (String identifier) {
|
| - return identifier != prefix && identifier.startsWith(prefix);
|
| - }).toList(growable: false);
|
| - results.sort();
|
| - if (results.isEmpty) results = <String>[prefix];
|
| -
|
| - results.forEach((String completion) {
|
| - staticResults.nodes.add(buildCompletionEntry(completion));
|
| - });
|
| -
|
| - if (settings.enableDartMind) {
|
| - // TODO(ahe): Move this code to its own function or class.
|
| - String encodedArg0 = Uri.encodeComponent('"$prefix"');
|
| - String mindQuery =
|
| - 'http://dart-mind.appspot.com/rpc'
|
| - '?action=GetExportingPubCompletions'
|
| - '&arg0=$encodedArg0';
|
| - try {
|
| - var serverWatch = new Stopwatch()..start();
|
| - HttpRequest.getString(mindQuery).then((String responseText) {
|
| - serverWatch.stop();
|
| - List<String> serverSuggestions = JSON.decode(responseText);
|
| - if (!serverSuggestions.isEmpty) {
|
| - updateInlineSuggestion(prefix, serverSuggestions.first);
|
| - }
|
| - var root = getShadowRoot(ui);
|
| - for (int i = 1; i < serverSuggestions.length; i++) {
|
| - String completion = serverSuggestions[i];
|
| - DivElement where = staticResults;
|
| - int index = results.indexOf(completion);
|
| - if (index != -1) {
|
| - List<Element> entries = root.querySelectorAll(
|
| - '.dart-static>.dart-entry');
|
| - entries[index].classes.add('doubleplusgood');
|
| - } else {
|
| - if (results.length > 3) {
|
| - serverResults.style.display = 'block';
|
| - where = serverResults;
|
| - }
|
| - Element entry = buildCompletionEntry(completion);
|
| - entry.classes.add('doubleplusgood');
|
| - where.nodes.add(entry);
|
| - }
|
| - }
|
| - serverResults.appendHtml(
|
| - '<div>${serverWatch.elapsedMilliseconds}ms</div>');
|
| - // Discard mutations.
|
| - observer.takeRecords();
|
| - }).catchError((error, stack) {
|
| - window.console.dir(error);
|
| - window.console.error('$stack');
|
| - });
|
| - } catch (error, stack) {
|
| - window.console.dir(error);
|
| - window.console.error('$stack');
|
| - }
|
| - }
|
| - // Discard mutations.
|
| - observer.takeRecords();
|
| - }
|
| -
|
| - Element buildCompletionEntry(String completion) {
|
| - return new DivElement()
|
| - ..classes.add('dart-entry')
|
| - ..appendText(completion);
|
| - }
|
| -
|
| - void transitionToInitialState() {
|
| - endCompletion();
|
| - }
|
| -}
|
| -
|
| -Token tokenize(String text) {
|
| - var file = new StringSourceFile.fromName('', text);
|
| - return new StringScanner(file, includeComments: true).tokenize();
|
| -}
|
| -
|
| -bool computeHasModifier(KeyboardEvent event) {
|
| - return
|
| - event.getModifierState("Alt") ||
|
| - event.getModifierState("AltGraph") ||
|
| - event.getModifierState("CapsLock") ||
|
| - event.getModifierState("Control") ||
|
| - event.getModifierState("Fn") ||
|
| - event.getModifierState("Meta") ||
|
| - event.getModifierState("NumLock") ||
|
| - event.getModifierState("ScrollLock") ||
|
| - event.getModifierState("Scroll") ||
|
| - event.getModifierState("Win") ||
|
| - event.getModifierState("Shift") ||
|
| - event.getModifierState("SymbolLock") ||
|
| - event.getModifierState("OS");
|
| -}
|
| -
|
| -String tokenizeAndHighlight(String line,
|
| - String state,
|
| - int start,
|
| - TrySelection trySelection,
|
| - List<Node> nodes) {
|
| - String newState = '';
|
| - int offset = state.length;
|
| - int adjustedStart = start - state.length;
|
| -
|
| - // + offset + charOffset + globalOffset + (charOffset + charCount)
|
| - // v v v v
|
| - // do identifier_abcdefghijklmnopqrst
|
| - for (Token token = tokenize('$state$line');
|
| - token.kind != EOF_TOKEN;
|
| - token = token.next) {
|
| - int charOffset = token.charOffset;
|
| - int charCount = token.charCount;
|
| -
|
| - Token tokenToDecorate = token;
|
| - if (token is UnterminatedToken && isUnterminatedMultiLineToken(token)) {
|
| - newState += '${token.start}';
|
| - continue; // This might not be an error.
|
| - } else {
|
| - Token follow = token.next;
|
| - if (token is BeginGroupToken && token.endGroup != null) {
|
| - follow = token.endGroup.next;
|
| - }
|
| - if (token.kind == STRING_TOKEN) {
|
| - follow = followString(follow);
|
| - if (follow is UnmatchedToken) {
|
| - if ('${follow.begin.value}' == r'${') {
|
| - newState += '${extractQuote(token.value)}';
|
| - }
|
| - }
|
| - }
|
| - if (follow is ErrorToken && follow.charOffset == token.charOffset) {
|
| - if (follow is UnmatchedToken) {
|
| - newState += '${follow.begin.value}';
|
| - } else {
|
| - tokenToDecorate = follow;
|
| - }
|
| - }
|
| - }
|
| -
|
| - if (charOffset < offset) {
|
| - // Happens for scanner errors, or for the [state] prefix.
|
| - continue;
|
| - }
|
| -
|
| - Decoration decoration;
|
| - if (charOffset - state.length == line.length - 1 && line.endsWith('\n')) {
|
| - // Don't add decorations to trailing newline.
|
| - decoration = null;
|
| - } else {
|
| - decoration = editor.getDecoration(tokenToDecorate);
|
| - }
|
| -
|
| - if (decoration == null) continue;
|
| -
|
| - // Add a node for text before current token.
|
| - trySelection.addNodeFromSubstring(
|
| - adjustedStart + offset, adjustedStart + charOffset, nodes);
|
| -
|
| - // Add a node for current token.
|
| - trySelection.addNodeFromSubstring(
|
| - adjustedStart + charOffset,
|
| - adjustedStart + charOffset + charCount, nodes, decoration);
|
| -
|
| - offset = charOffset + charCount;
|
| - }
|
| -
|
| - // Add a node for anything after the last (decorated) token.
|
| - trySelection.addNodeFromSubstring(
|
| - adjustedStart + offset, start + line.length, nodes);
|
| -
|
| - return newState;
|
| -}
|
| -
|
| -bool isUnterminatedMultiLineToken(UnterminatedToken token) {
|
| - return
|
| - token.start == '/*' ||
|
| - token.start == "'''" ||
|
| - token.start == '"""' ||
|
| - token.start == "r'''" ||
|
| - token.start == 'r"""';
|
| -}
|
| -
|
| -void normalizeMutationRecord(MutationRecord record,
|
| - TrySelection selection,
|
| - Set<Node> normalizedNodes) {
|
| - for (Node node in record.addedNodes) {
|
| - if (node.parentNode == null) continue;
|
| - normalizedNodes.add(findLine(node));
|
| - if (node is Text) continue;
|
| - StringBuffer buffer = new StringBuffer();
|
| - int selectionOffset = htmlToText(node, buffer, selection);
|
| - Text newNode = new Text('$buffer');
|
| - node.replaceWith(newNode);
|
| - if (selectionOffset != -1) {
|
| - selection.anchorNode = newNode;
|
| - selection.anchorOffset = selectionOffset;
|
| - }
|
| - }
|
| - if (!record.removedNodes.isEmpty) {
|
| - var first = record.removedNodes.first;
|
| - var line = findLine(record.target);
|
| -
|
| - if (first is Text && line.nextNode != null) {
|
| - normalizedNodes.add(line.nextNode);
|
| - }
|
| - normalizedNodes.add(line);
|
| - }
|
| - if (record.type == "characterData" && record.target.parentNode != null) {
|
| - // At least Firefox sends a "characterData" record whose target is the
|
| - // deleted text node. It also sends a record where "removedNodes" isn't
|
| - // empty whose target is the parent (which we are interested in).
|
| - normalizedNodes.add(findLine(record.target));
|
| - }
|
| -}
|
| -
|
| -// Finds the line of [node] (a parent node with CSS class 'lineNumber').
|
| -// If no such parent exists, return mainEditorPane if it is a parent.
|
| -// Otherwise return [node].
|
| -Node findLine(Node node) {
|
| - for (Node n = node; n != null; n = n.parentNode) {
|
| - if (n is Element && n.classes.contains('lineNumber')) return n;
|
| - if (n == mainEditorPane) return n;
|
| - }
|
| - return node;
|
| -}
|
| -
|
| -Element makeLine(List<Node> lineNodes, String state) {
|
| - return new SpanElement()
|
| - ..setAttribute('dart-state', state)
|
| - ..nodes.addAll(lineNodes)
|
| - ..classes.add('lineNumber');
|
| -}
|
| -
|
| -bool isAtEndOfFile(Text text, int offset) {
|
| - Node line = findLine(text);
|
| - return
|
| - line.nextNode == null &&
|
| - text.parentNode.nextNode == null &&
|
| - offset == text.length;
|
| -}
|
| -
|
| -List<String> splitLines(String text) {
|
| - return text.split(new RegExp('^', multiLine: true));
|
| -}
|
| -
|
| -void removeCodeCompletion() {
|
| - List<Node> highlighting =
|
| - mainEditorPane.querySelectorAll('.dart-code-completion');
|
| - for (Element element in highlighting) {
|
| - element.remove();
|
| - }
|
| -}
|
| -
|
| -bool isCompilerStageMarker(String message) {
|
| - return
|
| - message.startsWith('Package root is ') ||
|
| - message.startsWith('Compiling ') ||
|
| - message == "Resolving..." ||
|
| - message.startsWith('Resolved ') ||
|
| - message == "Inferring types..." ||
|
| - message == "Compiling..." ||
|
| - message.startsWith('Compiled ');
|
| -}
|
| -
|
| -void workAroundFirefoxBug() {
|
| - Selection selection = window.getSelection();
|
| - if (!isCollapsed(selection)) return;
|
| - Node node = selection.anchorNode;
|
| - int offset = selection.anchorOffset;
|
| - if (node is Element && offset != 0) {
|
| - // In some cases, Firefox reports the wrong anchorOffset (always seems to
|
| - // be 6) when anchorNode is an Element. Moving the cursor back and forth
|
| - // adjusts the anchorOffset.
|
| - // Safari can also reach this code, but the offset isn't wrong, just
|
| - // inconsistent. After moving the cursor back and forth, Safari will make
|
| - // the offset relative to a text node.
|
| - if (settings.hasSelectionModify.value) {
|
| - // IE doesn't support selection.modify, but it's okay since the code
|
| - // above is for Firefox, IE doesn't have problems with anchorOffset.
|
| - selection
|
| - ..modify('move', 'backward', 'character')
|
| - ..modify('move', 'forward', 'character');
|
| - print('Selection adjusted $node@$offset -> '
|
| - '${selection.anchorNode}@${selection.anchorOffset}.');
|
| - }
|
| - }
|
| -}
|
| -
|
| -/// Compute the token following a string. Compare to parseSingleLiteralString
|
| -/// in parser.dart.
|
| -Token followString(Token token) {
|
| - // TODO(ahe): I should be able to get rid of this if I change the scanner to
|
| - // create BeginGroupToken for strings.
|
| - int kind = token.kind;
|
| - while (kind != EOF_TOKEN) {
|
| - if (kind == STRING_INTERPOLATION_TOKEN) {
|
| - // Looking at ${expression}.
|
| - BeginGroupToken begin = token;
|
| - token = begin.endGroup.next;
|
| - } else if (kind == STRING_INTERPOLATION_IDENTIFIER_TOKEN) {
|
| - // Looking at $identifier.
|
| - token = token.next.next;
|
| - } else {
|
| - return token;
|
| - }
|
| - kind = token.kind;
|
| - if (kind != STRING_TOKEN) return token;
|
| - token = token.next;
|
| - kind = token.kind;
|
| - }
|
| - return token;
|
| -}
|
| -
|
| -String extractQuote(String string) {
|
| - StringQuoting q = StringValidator.quotingFromString(string);
|
| - return (q.raw ? 'r' : '') + (q.quoteChar * q.leftQuoteLength);
|
| -}
|
|
|