blob: abe3f3ca19cb9ac29fa00600d55d2487cb2e5cd1 [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 'package:compiler/src/scanner/string_scanner.dart' show
StringScanner;
import 'package:compiler/src/tokens/token.dart' show
ErrorToken,
Token;
import 'package:compiler/src/tokens/token_constants.dart' show
EOF_TOKEN;
import 'ui.dart' show
currentTheme,
hackDiv,
interaction,
mainEditorPane,
observer,
outputDiv;
import 'decoration.dart' show
CodeCompletionDecoration,
Decoration,
DiagnosticDecoration,
error,
info,
warning;
import 'selection.dart' show
isCollapsed;
import 'shadow_root.dart' show
getShadowRoot;
import 'settings.dart' as settings;
const String INDENT = '\u{a0}\u{a0}';
Set<String> seenIdentifiers;
Element moveActive(int distance, Node ui) {
var /* ShadowRoot or Element */ root = getShadowRoot(ui);
List<Element> entries = root.querySelectorAll('.dart-static>.dart-entry');
int activeIndex = -1;
for (var i = 0; i < entries.length; i++) {
if (entries[i].classes.contains('activeEntry')) {
activeIndex = i;
break;
}
}
int newIndex = activeIndex + distance;
Element currentEntry;
if (0 <= newIndex && newIndex < entries.length) {
currentEntry = entries[newIndex];
}
if (currentEntry == null) return null;
if (0 <= newIndex && activeIndex != -1) {
entries[activeIndex].classes.remove('activeEntry');
}
Element staticNode = root.querySelector('.dart-static');
String visibility = computeVisibility(currentEntry, staticNode);
print(visibility);
var serverResults = root.querySelectorAll('.dart-server>.dart-entry');
var serverResultCount = serverResults.length;
if (serverResultCount > 0) {
switch (visibility) {
case obscured:
case hidden: {
Rectangle cr = currentEntry.getBoundingClientRect();
Rectangle sr = staticNode.getBoundingClientRect();
Element entry = serverResults[0];
entry.remove();
currentEntry.parentNode.insertBefore(entry, currentEntry);
currentEntry = entry;
serverResultCount--;
staticNode.style.maxHeight = '${sr.boundingBox(cr).height}px';
}
}
} else {
currentEntry.scrollIntoView();
}
if (serverResultCount == 0) {
root.querySelector('.dart-server').style.display = 'none';
}
if (currentEntry != null) {
currentEntry.classes.add('activeEntry');
}
// Discard mutations.
observer.takeRecords();
return currentEntry;
}
const visible = 'visible';
const obscured = 'obscured';
const hidden = 'hidden';
String computeVisibility(Element node, [Element parent]) {
Rectangle nr = node.getBoundingClientRect();
if (parent == null) parent = node.parentNode;
Rectangle pr = parent.getBoundingClientRect();
if (pr.containsRectangle(nr)) return visible;
if (pr.intersects(nr)) return obscured;
return hidden;
}
var activeCompletion;
num minSuggestionWidth = 0;
/// Returns the [Element] which encloses the current collapsed selection, if it
/// exists.
Element getElementAtSelection() {
Selection selection = window.getSelection();
if (!isCollapsed(selection)) return null;
var anchorNode = selection.anchorNode;
if (!mainEditorPane.contains(anchorNode)) return null;
if (mainEditorPane == anchorNode) return null;
int type = anchorNode.nodeType;
if (type != Node.TEXT_NODE) return null;
Text text = anchorNode;
var parent = text.parent;
if (parent is! Element) return null;
if (mainEditorPane == parent) return null;
return parent;
}
bool isMalformedInput = false;
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;
var alert;
if (kind == 'error') {
alert = error(message);
} else if (kind == 'warning') {
alert = warning(message);
} else {
alert = info(message);
}
Element parent = node.parentNode;
if (parent.classes.contains("diagnostic") &&
!interaction.oldDiagnostics.contains(parent)) {
Element other = parent.firstChild;
other.remove();
SpanElement wrapper = new SpanElement()
..classes.add('diagnostic')
..style.fontWeight = 'normal';
var root = getShadowRoot(wrapper);
if (root is ShadowRoot) {
// When https://code.google.com/p/chromium/issues/detail?id=313458
// is fixed:
// var link = new LinkElement()
// ..rel = "stylesheet"
// ..type = "text/css"
// ..href = "dartlang-style.css";
// root.append(link);
root.append(
new StyleElement()..text = '@import url(dartlang-style.css)');
}
root
..append(other)
..append(alert);
other.style.display = 'block';
alert.style.display = 'block';
parent.append(wrapper);
} else {
if (interaction.oldDiagnostics.contains(parent)) {
node.remove();
parent.replaceWith(node);
}
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).
marker.replaceWith(diagnostic(node, alert));
if (hasSelection) {
selection.collapse(node, anchorOffset);
}
}
foundNode = true;
return;
}
offset = newOffset;
} else if (type == Node.ELEMENT_NODE) {
Element element = node;
CssClassSet classes = element.classes;
if (classes.contains('alert') ||
classes.contains('dart-code-completion')) {
return;
}
}
var child = node.firstChild;
while(child != null && !foundNode) {
walk4(child);
child = child.nextNode;
}
}
walk4(mainEditorPane);
if (!foundNode) {
outputDiv.appendText('$message\n');
}
observer.takeRecords();
observer.observe(
mainEditorPane, childList: true, characterData: true, subtree: true);
}
Decoration getDecoration(Token token) {
if (token is ErrorToken) {
// TODO(ahe): Remove side effects from this method. It only leads to
// confusion.
isMalformedInput = true;
return new DiagnosticDecoration('error', token.assertionMessage);
}
String tokenValue = token.value;
String tokenInfo = token.info.value;
if (tokenInfo == 'string') return currentTheme.string;
if (tokenInfo == 'identifier') {
seenIdentifiers.add(tokenValue);
Decoration decoration = currentTheme.foreground;
if (settings.enableCodeCompletion.value) {
decoration = CodeCompletionDecoration.from(decoration);
}
return decoration;
}
if (tokenInfo == 'keyword') return currentTheme.keyword;
if (tokenInfo == 'comment') return currentTheme.singleLineComment;
if (tokenInfo == 'malformed input') {
// TODO(ahe): Remove side effects from this method. It only leads to
// confusion.
isMalformedInput = true;
return new DiagnosticDecoration('error', tokenValue);
}
return currentTheme.foreground;
}
diagnostic(content, tip) {
if (content is String) {
content = new Text(content);
}
if (content is! List) {
content = [content];
}
return new AnchorElement()
..classes.add('diagnostic')
..append(tip) // Should be first for better Firefox editing.
..nodes.addAll(content);
}