blob: 885295de9c167a69860620832964cdb048f71f0c [file] [log] [blame]
// Copyright (c) 2013, 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.editor;
import 'dart:html';
import '../../../sdk/lib/_internal/compiler/implementation/scanner/scannerlib.dart'
show
EOF_TOKEN,
StringScanner,
Token;
import '../../../sdk/lib/_internal/compiler/implementation/source_file.dart' show
StringSourceFile;
import 'compilation.dart' show
scheduleCompilation;
import 'ui.dart' show
currentTheme,
hackDiv,
inputPre,
observer,
outputDiv;
import 'decoration.dart' show
Decoration,
DiagnosticDecoration,
error,
info,
warning;
const String INDENT = '\u{a0}\u{a0}';
onKeyUp(KeyboardEvent e) {
if (e.keyCode == 13) {
e.preventDefault();
Selection selection = window.getSelection();
if (selection.isCollapsed && selection.anchorNode is Text) {
Text text = selection.anchorNode;
int offset = selection.anchorOffset;
text.insertData(offset, '\n');
selection.collapse(text, offset + 1);
}
}
// This is a hack to get Safari to send mutation events on contenteditable.
var newDiv = new DivElement();
hackDiv.replaceWith(newDiv);
hackDiv = newDiv;
}
bool isMalformedInput = false;
String currentSource = "";
// TODO(ahe): This method should be cleaned up. It is too large.
onMutation(List<MutationRecord> mutations, MutationObserver observer) {
scheduleCompilation();
for (Element element in inputPre.queryAll('a[class="diagnostic"]>span')) {
element.remove();
}
// Discard clean-up mutations.
observer.takeRecords();
Selection selection = window.getSelection();
while (!mutations.isEmpty) {
for (MutationRecord record in mutations) {
String type = record.type;
switch (type) {
case 'characterData':
bool hasSelection = false;
int offset = selection.anchorOffset;
if (selection.isCollapsed && selection.anchorNode == record.target) {
hasSelection = true;
}
var parent = record.target.parentNode;
if (parent != inputPre) {
inlineChildren(parent);
}
if (hasSelection) {
selection.collapse(record.target, offset);
}
break;
default:
if (!record.addedNodes.isEmpty) {
for (var node in record.addedNodes) {
if (node.nodeType != Node.ELEMENT_NODE) continue;
if (node is BRElement) {
if (selection.anchorNode != node) {
node.replaceWith(new Text('\n'));
}
} else {
var parent = node.parentNode;
if (parent == null) continue;
var nodes = new List.from(node.nodes);
var style = node.getComputedStyle();
if (style.display != 'inline') {
var previous = node.previousNode;
if (previous is Text) {
previous.appendData('\n');
} else {
parent.insertBefore(new Text('\n'), node);
}
}
for (Node child in nodes) {
child.remove();
parent.insertBefore(child, node);
}
node.remove();
}
}
}
}
}
mutations = observer.takeRecords();
}
if (!inputPre.nodes.isEmpty && inputPre.nodes.last is Text) {
Text text = inputPre.nodes.last;
if (!text.text.endsWith('\n')) {
text.appendData('\n');
}
}
int offset = 0;
int anchorOffset = 0;
bool hasSelection = false;
Node anchorNode = selection.anchorNode;
// TODO(ahe): Try to share walk4 methods.
void walk4(Node node) {
// TODO(ahe): Use TreeWalker when that is exposed.
// function textNodesUnder(root){
// var n, a=[], walk=document.createTreeWalker(
// root,NodeFilter.SHOW_TEXT,null,false);
// while(n=walk.nextNode()) a.push(n);
// return a;
// }
int type = node.nodeType;
if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
CharacterData text = node;
if (anchorNode == node) {
hasSelection = true;
anchorOffset = selection.anchorOffset + offset;
return;
}
offset += text.length;
}
var child = node.firstChild;
while (child != null) {
walk4(child);
if (hasSelection) return;
child = child.nextNode;
}
}
if (selection.isCollapsed) {
walk4(inputPre);
}
currentSource = inputPre.text;
inputPre.nodes.clear();
inputPre.appendText(currentSource);
if (hasSelection) {
selection.collapse(inputPre.firstChild, anchorOffset);
}
isMalformedInput = false;
for (var n in new List.from(inputPre.nodes)) {
if (n is! Text) continue;
Text node = n;
String text = node.text;
Token token = new StringScanner(
new StringSourceFile('', text), includeComments: true).tokenize();
int offset = 0;
for (;token.kind != EOF_TOKEN; token = token.next) {
Decoration decoration = getDecoration(token);
if (decoration == null) continue;
bool hasSelection = false;
int selectionOffset = selection.anchorOffset;
if (selection.isCollapsed && selection.anchorNode == node) {
hasSelection = true;
selectionOffset = selection.anchorOffset;
}
int splitPoint = token.charOffset - offset;
Text str = node.splitText(splitPoint);
Text after = str.splitText(token.charCount);
offset += splitPoint + token.charCount;
inputPre.insertBefore(after, node.nextNode);
inputPre.insertBefore(decoration.applyTo(str), after);
if (hasSelection && selectionOffset > node.length) {
selectionOffset -= node.length;
if (selectionOffset > str.length) {
selectionOffset -= str.length;
selection.collapse(after, selectionOffset);
} else {
selection.collapse(str, selectionOffset);
}
}
node = after;
}
}
window.localStorage['currentSource'] = currentSource;
// Discard highlighting mutations.
observer.takeRecords();
}
addDiagnostic(String kind, String message, int begin, int end) {
observer.disconnect();
Selection selection = window.getSelection();
int offset = 0;
int anchorOffset = 0;
bool hasSelection = false;
Node anchorNode = selection.anchorNode;
bool foundNode = false;
void walk4(Node node) {
// TODO(ahe): Use TreeWalker when that is exposed.
int type = node.nodeType;
if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE) {
CharacterData cdata = node;
// print('walking: ${node.data}');
if (anchorNode == node) {
hasSelection = true;
anchorOffset = selection.anchorOffset + offset;
}
int newOffset = offset + cdata.length;
if (offset <= begin && begin < newOffset) {
hasSelection = node == anchorNode;
anchorOffset = selection.anchorOffset;
Node marker = new Text("");
node.replaceWith(marker);
// TODO(ahe): Don't highlight everything in the node. Find the
// relevant token (works for now as we create a node for each token,
// which is probably not great for performance).
if (kind == 'error') {
marker.replaceWith(diagnostic(node, error(message)));
} else if (kind == 'warning') {
marker.replaceWith(diagnostic(node, warning(message)));
} else {
marker.replaceWith(diagnostic(node, info(message)));
}
if (hasSelection) {
selection.collapse(node, anchorOffset);
}
foundNode = true;
return;
}
offset = newOffset;
} else if (type == Node.ELEMENT_NODE) {
Element element = node;
if (element.classes.contains('alert')) return;
}
var child = node.firstChild;
while(child != null && !foundNode) {
walk4(child);
child = child.nextNode;
}
}
walk4(inputPre);
if (!foundNode) {
outputDiv.appendText('$message\n');
}
observer.takeRecords();
observer.observe(
inputPre, childList: true, characterData: true, subtree: true);
}
void inlineChildren(Element element) {
if (element == null) return;
var parent = element.parentNode;
if (parent == null) return;
for (Node child in new List.from(element.nodes)) {
child.remove();
parent.insertBefore(child, element);
}
element.remove();
}
Decoration getDecoration(Token token) {
String tokenValue = token.value;
String tokenInfo = token.info.value;
if (tokenInfo == 'string') return currentTheme.string;
// if (tokenInfo == 'identifier') return identifier;
if (tokenInfo == 'keyword') return currentTheme.keyword;
if (tokenInfo == 'comment') return currentTheme.singleLineComment;
if (tokenInfo == 'malformed input') {
isMalformedInput = true;
return new DiagnosticDecoration('error', tokenValue);
}
return currentTheme.foreground;
}
diagnostic(text, tip) {
if (text is String) {
text = new Text(text);
}
return new AnchorElement()
..classes.add('diagnostic')
..append(text)
..append(tip);
}