Better handling of large files.

R=kasperl@google.com

Review URL: https://codereview.chromium.org//265063002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@35801 260f80e4-7a28-3924-810f-c04153c831b5
This commit is contained in:
ahe@google.com 2014-05-06 12:58:28 +00:00
parent 5e65be57fa
commit d8d2d2978a
5 changed files with 257 additions and 19 deletions

View file

@ -14,6 +14,7 @@ TODO(ahe): Reduce the number of fonts used based on actual usage.
See: http://www.google.com/fonts#UsePlace:use/Collection:Open+Sans:400,600,700,800,300
-->
<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,600,700,800,300' rel='stylesheet' type='text/css'>
<link rel="stylesheet" type="text/css" href="dartlang-style.css">
<link rel="alternate stylesheet" type="text/css" href="line_numbers.css" title="line_numbers">
@ -34,7 +35,7 @@ a:hover.diagnostic span {
position: absolute;
/* left: 1em; */
/* top: 2em; */
left: 10px;
right: 10px;
}
.offline {
@ -211,7 +212,7 @@ a:hover.diagnostic span {
<a class="brand" href="//www.dartlang.org/" title="Dart Homepage" target="_blank">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAMAAACeyVWkAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJxQTFRFAAAAAIvMdsvDAIvMdsvDAIvMdsvDLaTJAIvMOqnHdsvDAIvMdsvDAIvMKaLJdsvDAIvMAIvMdsvDAIvMdsvDdsvDAIvMAIvMAZnFdsvDAILHAIPHAITIAIXJAIfKAIjKAIrLAIrMAIvMAJXHAJjFC5i/I6HENr2yOb6zPr+0TsK4UsO5WbnEWcW8Xsa9Yse+Zsi/asjAc8rCdsvDdt4SRQAAABp0Uk5TABAQICAwMFBgYGBwcICAgI+vr7+/z9/v7+97IXGnAAAAqUlEQVQYV13QxxaCQBBE0VZkjBgAGVEBaVEUM/P//yaTGg5vV3dZANTCZ9BvFAoR93kVC9FnthW6uIPTJ7UkdHaXvS2LXKNBURInyDXPsShbzjU7XCpxhooDVGo5QcQAJmjUco64AY/UcIrowYCTaj5KBZeTaj5JBTc6l11OlQKMf497y1ahefFb3TQfcqtM/fipJF/X9gnDon6/ah/aDDfNOgosNA2b8QdGciZlh/U93AAAAABJRU5ErkJggg==" alt="Dart">
</a>
<ul class="nav pull-right"><li><a href="#" id="settings"><i class="icon-cog"></i></a></li></ul>
<ul class="nav pull-right"><li><a href="https://code.google.com/p/dart/issues/entry?template=Try+Dart+Bug" target="_blank"><i class="fa fa-bug"></i></a></li><li><a href="#" id="settings"><i class="icon-cog"></i></a></li></ul>
<ul class="nav hidden-phone">
<li class="active"><a>Try Dart!</a></li>

View file

@ -5,9 +5,6 @@
.mainEditorPane {
padding: 0px;
}
div {
counter-reset: dart-line-number;
}

View file

@ -92,6 +92,8 @@ abstract class InteractionManager {
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);
@ -228,13 +230,28 @@ class InitialState extends InteractionState {
void onUnmodifiedKeyUp(KeyboardEvent event) {
switch (event.keyCode) {
case KeyCode.ENTER: {
event.preventDefault();
Selection selection = window.getSelection();
if (isCollapsed(selection) && selection.anchorNode is Text) {
Text text = selection.anchorNode;
int offset = selection.anchorOffset;
text.insertData(offset, '\n');
selection.collapse(text, offset + 1);
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;
}
@ -262,8 +279,46 @@ class InitialState extends InteractionState {
Selection selection = window.getSelection();
TrySelection trySelection = new TrySelection(mainEditorPane, selection);
Set<Node> normalizedNodes = new Set<Node>();
for (MutationRecord record in mutations) {
normalizeMutationRecord(record, trySelection);
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}');
String currentText = node.text;
trySelection = new TrySelection(node, selection);
trySelection.updateText(currentText);
editor.isMalformedInput = false;
int offset = 0;
List<Node> nodes = <Node>[];
String state = '';
Element previousLine = node.previousElementSibling;
if (previousLine != null) {
state = previousLine.getAttribute('dart-state');
}
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));
}
node.parent.insertAllBefore(nodes, node);
node.remove();
trySelection.adjust(selection);
// Discard highlighting mutations.
observer.takeRecords();
return;
}
}
String currentText = mainEditorPane.text;
@ -278,14 +333,12 @@ class InitialState extends InteractionState {
List<Node> nodes = <Node>[];
String state = '';
for (String line in currentText.split(new RegExp('^', multiLine: true))) {
for (String line in splitLines(currentText)) {
List<Node> lineNodes = <Node>[];
state =
tokenizeAndHighlight(line, state, offset, trySelection, lineNodes);
offset += line.length;
nodes.add(new SpanElement()
..nodes.addAll(lineNodes)
..classes.add('lineNumber'));
nodes.add(makeLine(lineNodes, state));
}
mainEditorPane
@ -785,17 +838,60 @@ bool isUnterminatedMultiLineToken(UnterminatedToken token) {
token.start == 'r"""';
}
void normalizeMutationRecord(MutationRecord record, TrySelection selection) {
if (record.addedNodes.isEmpty) return;
void normalizeMutationRecord(MutationRecord record,
TrySelection selection,
Set<Node> normalizedNodes) {
for (Node node in record.addedNodes) {
if (node.parent == null) continue;
StringBuffer buffer = new StringBuffer();
int selectionOffset = htmlToText(node, buffer, selection);
Text newNode = new Text('$buffer');
node.replaceWith(newNode);
normalizedNodes.add(findLine(newNode));
if (selectionOffset != -1) {
selection.anchorNode = newNode;
selection.anchorOffset = selectionOffset;
}
}
if (!record.removedNodes.isEmpty) {
normalizedNodes.add(findLine(record.target));
}
if (record.type == "characterData") {
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.parent) {
if (n is Element && n.classes.contains('lineNumber')) return n;
if (n == mainEditorPane) return n;
}
return node;
}
Element makeLine(List<Node> lineNodes, String state) {
// Using a div element here (anything with display=block) generally messes up
// editing and navigation. We would like to use a block element here so
// error messages show as expected. But no such luck. Fortunately, there
// are strong indications that the current solution for displaying errors
// isn't good enough anyways.
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.parent.nextNode == null &&
offset == text.length;
}
List<String> splitLines(String text) {
return text.split(new RegExp('^', multiLine: true));
}

View file

@ -34,7 +34,7 @@ class TrySelection {
Text textNode = new Text(text.substring(start, end));
if (start <= globalOffset && globalOffset < end) {
if (start <= globalOffset && globalOffset <= end) {
anchorNode = textNode;
anchorOffset = globalOffset - start;
}

View file

@ -0,0 +1,144 @@
// 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.
// SharedOptions=--package-root=sdk/lib/_internal/
// Test that cursor positions are correctly updated after adding new content.
library trydart.cursor_position_test;
import 'dart:html';
import 'dart:async';
import '../../site/try/src/interaction_manager.dart' show
InteractionManager;
import '../../site/try/src/ui.dart' show
hackDiv,
mainEditorPane,
observer;
import '../../site/try/src/user_option.dart' show
UserOption;
import '../../pkg/expect/lib/expect.dart';
import '../../pkg/async_helper/lib/async_helper.dart';
void main() {
InteractionManager interaction = mockTryDartInteraction();
List<TestCase> tests = <TestCase>[
new TestCase('Test adding two lines programmatically.', () {
clearEditorPaneWithoutNotifications();
mainEditorPane.appendText('\n\n');
Text text = mainEditorPane.firstChild;
window.getSelection().collapse(text, 1);
checkSelectionIsCollapsed(text, 1);
}, checkAtBeginningOfSecondLine),
new TestCase('Test adding a new line with mock key event.', () {
clearEditorPaneWithoutNotifications();
checkSelectionIsCollapsed(mainEditorPane, 0);
simulateEnterKeyDown(interaction);
}, checkAtBeginningOfSecondLine),
];
runTests(tests.iterator, completerForAsyncTest());
}
void simulateEnterKeyDown(Interaction interaction) {
interaction.onKeyUp(
new MockKeyboardEvent('keydown', keyCode: KeyCode.ENTER));
}
void clearEditorPaneWithoutNotifications() {
mainEditorPane.nodes.clear();
observer.takeRecords();
}
void checkSelectionIsCollapsed(Node node, int offset) {
var selection = window.getSelection();
Expect.isTrue(selection.isCollapsed, 'selection.isCollapsed');
Expect.equals(node, selection.anchorNode, 'selection.anchorNode');
Expect.equals(offset, selection.anchorOffset, 'selection.anchorOffset');
}
void checkLineCount(int expectedLineCount) {
Expect.equals(
expectedLineCount, mainEditorPane.nodes.length,
'mainEditorPane.nodes.length');
}
void checkAtBeginningOfSecondLine() {
checkLineCount(2);
checkSelectionIsCollapsed(mainEditorPane.nodes[1].firstChild, 0);
}
runTests(Iterator<TestCase> iterator, Completer completer) {
if (iterator.moveNext()) {
TestCase test = iterator.current;
new Future(() {
print('${test.description}\nSetup.');
test.setup();
new Future(() {
test.validate();
print('${test.description}\nDone.');
runTests(iterator, completer);
});
});
} else {
completer.complete(null);
}
}
Completer completerForAsyncTest() {
Completer completer = new Completer();
asyncTest(() => completer.future.then((_) {
// Clear the DOM to work around a bug in test.dart.
document.body.nodes.clear();
}));
return completer;
}
InteractionManager mockTryDartInteraction() {
UserOption.storage = {};
InteractionManager interaction = new InteractionManager();
hackDiv = new DivElement();
mainEditorPane = new DivElement()
..style.whiteSpace = 'pre'
..contentEditable = 'true';
observer = new MutationObserver(interaction.onMutation);
observer.observe(
mainEditorPane, childList: true, characterData: true, subtree: true);
document.body.nodes.addAll([mainEditorPane, hackDiv]);
return interaction;
}
class MockKeyboardEvent extends KeyEvent {
final int keyCode;
MockKeyboardEvent(String type, {int keyCode})
: this.keyCode = keyCode,
super.wrap(new KeyEvent(type, keyCode: keyCode));
bool getModifierState(String keyArgument) => false;
}
typedef void VoidFunction();
class TestCase {
final String description;
final VoidFunction setup;
final VoidFunction validate;
TestCase(this.description, this.setup, this.validate);
}