Files
sdk/utils/tip/tip.dart
T
jmesserly@google.com 20c88fd220 Fix path in tip
I broke this when I moved to utils... fortunately an easy fix!

Review URL: http://codereview.chromium.org//8930015

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@2396 260f80e4-7a28-3924-810f-c04153c831b5
2011-12-13 20:48:15 +00:00

1625 lines
41 KiB
Dart

// Copyright (c) 2011, 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.
/**
* This giant file has pieces for compiling dart in the browser, injecting
* scripts into pages and a bunch of classes to build a simple dart editor.
*
* TODO(jimhug): Separate these pieces cleanly.
*/
#import('dart:dom');
#import('../../frog/lang.dart');
#import('../../frog/file_system_dom.dart');
void main() {
final systemPath = getRootPath(window) + '/../../frog';
final userPath = getRootPath(window.parent);
if (window !== window.parent) {
// I'm in an iframe - frogify my surrounding code unless dart is native.
if (!document.implementation.hasFeature('dart', '')) {
// Suppress warnings to avoid diff-based tests from failing on them.
initialize(systemPath, userPath, ['--suppress_warnings']);
window.addEventListener('DOMContentLoaded',
(e) => frogify(window.parent),
false);
}
} else {
// I'm at the top level - run the tip shell.
shell = new Shell();
document.body.appendChild(shell._node);
initialize(systemPath, userPath);
world.messageHandler = shell.handleMessage;
}
}
/** The filename to use for Dart code compiled directly from the browser. */
final String DART_FILENAME = 'input.dart';
/** Helper to get my base url. */
String getRootPath(Window window) {
String url = window.location.href;
final tail = url.lastIndexOf('/', url.length);
final dir = url.substring(0, tail);
return dir;
}
/**
* Invoke this script in the given window's context. Use
* this frame's window by default.
*/
void inject(String code, Window win, [String name = 'generated.js']) {
final doc = win.document;
var script = doc.createElement('script');
// TODO(vsm): Enable debugging of injected code. This sourceURL
// trick only appears to work for eval'ed code, not script injected
// code.
// Append sourceURL to enable debugging.
script.innerHTML = code + '\n//@ sourceURL=$name';
script.type = 'application/javascript';
doc.body.appendChild(script);
}
/**
* Compile all dart scripts in a window and inject/invoke the corresponding JS.
*/
void frogify(Window win) {
var doc = win.document;
int n = doc.scripts.length;
// TODO(vsm): Implement foreach iteration on native DOM types. This
// should be for (var script in doc.scripts) { ... }.
for (int i = 0; i < n; ++i) {
final script = doc.scripts[i];
if (script.type == 'application/dart') {
final src = script.src;
var input;
var name;
if (src == '') {
input = script.innerHTML;
name = null;
} else {
input = world.files.readAll(src);
name = '$src.js';
}
world.files.writeString(DART_FILENAME, input);
world.reset();
var success = world.compile();
if (success) {
inject(world.getGeneratedCode(), win, name);
} else {
inject('window.alert("compilation failed");', win, name);
}
}
}
}
void initialize(String systemPath, String userPath,
[List<String> flags = const []]) {
DomFileSystem fs = new DomFileSystem(userPath);
// TODO(jimhug): Workaround lib path hack in frog_options.dart
final options = [null, null, '--libdir=$systemPath/lib'];
options.addAll(flags);
options.add(DART_FILENAME);
parseOptions(systemPath, options, fs);
initializeWorld(fs);
}
final int LINE_HEIGHT = 22; // TODO(jimhug): This constant sucks.
final int CHAR_WIDTH = 8; // TODO(jimhug): See above.
final String CODE = '''
#import("dart:dom");
// This is an interesting field;
final int y = 22;
String name;
/** This is my main method. */
void main() {
var element = document.createElement('div');
element.innerHTML = "Hello dom from Dart!";
document.body.appendChild(element);
HTMLCanvasElement canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var context = canvas.getContext('2d');
context.setFillColor('purple');
context.fillRect(10, 10, 30, 30);
}
/**
* The usual method of computing factorial in the slowest possible way.
*/
num fact(n) {
if (n == 0) return 1;
return n * fact(n - 1);
}
final x = 22;
''';
final String HCODE = '''#import("dart:html");
void main() {
var element = document.createElement('div');
element.innerHTML = "Hello html from Dart!";
document.body.nodes.add(element);
CanvasElement canvas = document.createElement('canvas');
canvas.width = 100;
canvas.height = 100;
document.body.nodes.add(canvas);
var context = canvas.getContext('2d');
context.setFillColor('blue');
context.fillRect(10, 10, 30, 30);
}
''';
class Message {
var _node;
String prefix, message;
SourceSpan span;
Message(this.prefix, this.message, this.span) {
var col = prefix.indexOf(':');
if (col != -1) {
prefix = prefix.substring(0, col);
}
_node = document.createElement('div');
_node.className = 'message ${prefix}';
_node.innerText = '$message at ${span.locationText}';
_node.addEventListener('click', click, false);
}
void click(MouseEvent event) {
shell.showSpan(span);
}
}
class MessageWindow {
var _node;
List<Message> messages;
MessageWindow() {
messages = [];
_node = document.createElement('div');
_node.className = 'errors';
}
addMessage(Message message) {
messages.add(message);
_node.appendChild(message._node);
}
clear() {
messages.length = 0;
_node.innerHTML = '';
}
}
// TODO(jimhug): Type is needed.
Shell shell;
class Shell {
var _textInputArea;
KeyBindings _bindings;
Cursor cursor;
Editor _editor;
var _node;
var _output;
var _repl;
var _errors;
Shell() {
_node = document.createElement('div');
_node.className = 'shell';
_editor = new Editor(this);
_editor._node.style.setProperty('height', '93%');
_node.appendChild(_editor._node);
_textInputArea = document.createElement('textarea');
_textInputArea.className = 'hiddenTextArea';
_node.appendChild(_textInputArea);
var outDiv = document.createElement('div');
outDiv.className = 'output';
outDiv.style.setProperty('height', '83%');
_output = document.createElement('iframe');
outDiv.appendChild(_output);
_node.appendChild(outDiv);
_repl = document.createElement('div');
_repl.className = 'repl';
_repl.style.setProperty('height', '5%');
_repl.innerHTML = '<div>REPL Under Construction...</div>';
_node.appendChild(_repl);
_errors = new MessageWindow();
//_errors.innerHTML = '<h3>Errors/Warnings Under Construction...</h3>';
_errors._node.style.setProperty('height', '15%');
_node.appendChild(_errors._node);
// TODO(jimhug): Ick!
window.setTimeout( () {
_editor.focus();
_output.contentDocument.head.innerHTML = '''
<style>body {
font-family: arial , sans-serif;
}
h3 {
text-align: center;
}
</style>''';
_output.contentDocument.body.innerHTML = '<h3>Output will go here</h3>';
}, .5);
// TODO(jimhug): These are hugely incomplete and Mac-centric.
var bindings = {
'Left': () {
cursor.clearSelection();
cursor._pos = cursor._pos.moveColumn(-1);
},
'Shift-Left': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = cursor._pos.moveColumn(-1);
},
'Right': () {
cursor.clearSelection();
cursor._pos = cursor._pos.moveColumn(+1);
},
'Shift-Right': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = cursor._pos.moveColumn(+1);
},
'Up': () {
cursor.clearSelection();
// TODO(jimhug): up and down lose column info on shorter lines.
cursor._pos = cursor._pos.moveLine(-1);
},
'Shift-Up': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = cursor._pos.moveLine(-1);
},
'Down': () {
cursor.clearSelection();
cursor._pos = cursor._pos.moveLine(+1);
},
'Shift-Down': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = cursor._pos.moveLine(+1);
},
'Meta-Up': () {
cursor.clearSelection();
cursor._pos = _editor._code.start;
},
'Meta-Shift-Up': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = _editor._code.start;
},
'Meta-Down': () {
cursor.clearSelection();
cursor._pos = _editor._code.end;
},
'Meta-Shift-Down': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos;
}
cursor._pos = _editor._code.end;
},
'Delete': () {
//TODO(jimhug): go back to beginning of line when appropriate.
if (cursor._toPos == null) {
cursor._toPos = cursor._pos.moveColumn(-1);
}
cursor.deleteSelection();
},
'Control-D': () {
if (cursor._toPos == null) {
cursor._toPos = cursor._pos.moveColumn(+1);
}
cursor.deleteSelection();
},
'Meta-A': () {
cursor._pos = _editor._code.start;
cursor._toPos = _editor._code.end;
},
'.' : () {
// TODO(jimhug): complete hints here
cursor.write('.');
},
'}' : () {
// TODO(jimhug): Get indentation right.
cursor.write('}');
},
'Enter': () {
cursor.write('\n');
// TODO(jimhug): Indent on the new line appropriately...
},
'Tab': () {
// TODO(jimhug): force tab to always "properly" indent the line
cursor.write(' ');
},
'Space': () => cursor.write(' '),
// This seems to be a common typo, so just allow it.
'Shift-Space': () => cursor.write(' '),
'Meta-G': () => cursor.write(CODE),
'Meta-H': () => cursor.write(HCODE),
'Meta-P': () => cursor._pos.block.parse(),
'Shift-Enter': run,
'Meta-Enter': run,
};
_bindings = new KeyBindings(_textInputArea, bindings,
(String text) { cursor.write(text); },
(String key) {
// Use native bindings for cut and paste
if (key == 'Meta-V') return false;
if (key == 'Control-V') return false;
// TODO(jimhug): No good, very bad hack.
if (key == 'Meta-X' || key == 'Control-X') {
window.setTimeout(() {
cursor.deleteSelection();
_editor._redraw();
}, 0);
return false;
}
if (key == 'Meta-C') return false;
if (key == 'Control-C') return false;
// Use native bindings for dev tools
if (key == 'Alt-Meta-I') return false;
if (key == 'Control-Shift-I') return false;
window.console.log('Unbound key "$key"');
return true;
});
}
void handleMessage(String prefix, String message, SourceSpan span) {
var m = new Message(prefix, message, span);
_errors.addMessage(m);
}
void showSpan(SourceSpan span) {
// TODO(jimhug): Get code from correct file.
var code = _editor._code;
var p1 = new CodePosition(code, span.start);
var p2 = new CodePosition(code, span.end);
_editor._cursor._pos = p1.toLeaf();
_editor._cursor._toPos = p2.toLeaf();
_editor._redraw();
}
void run() {
_output.contentDocument.body.innerHTML =
'<h3 style="color:green">Compiling...</h3>';
_errors.clear();
window.setTimeout( () {
final sw = new Stopwatch();
sw.start();
var code = _editor.getCode();
world.files.writeString(DART_FILENAME, code);
options.enableAsserts = true;
options.enableTypeChecks = true;
world.reset();
var success = world.compile();
sw.stop();
if (success) {
_output.contentDocument.body.innerHTML = '';
inject(world.getGeneratedCode(), _output.contentWindow);
} else {
_output.contentDocument.body.innerHTML =
'<h3 style="color:red">Compilation failed</h3>';
}
print('compiled in ${sw.elapsedInMs()}msec');
}, 0);
}
void focusKeys(Editor editor) {
_editor = editor;
cursor = editor._cursor;
_textInputArea.focus();
}
}
class Editor {
Shell _shell;
Cursor _cursor;
CodeBlock _code;
var _node;
// Some temp state for mouse down and selections
bool _isSelecting;
int _lastClickTime;
bool _didDoubleClick;
Editor(this._shell) {
_node = document.createElement('div');
_node.className = 'editor';
_code = new BlockBlock(null, 0);
_code.text = CODE;
_code.top = 0;
_node.appendChild(_code._node);
_cursor = new Cursor(_code.start);
_node.appendChild(_cursor._node);
_node.addEventListener('mousedown', mousedown, false);
_node.addEventListener('mousemove', mousemove, false);
_node.addEventListener('mouseup', mouseup, false);
// TODO(jimhug): Final bit to make region selection clean.
//this.node.addEventListener('mouseout', this, false);
// TODO(jimhug): Lazy rendering should be triggered by this.
//_node.addEventListener('scroll', this, false);
}
String getCode() {
return _code.text;
}
void goto(int line, int column) {
_cursor._pos = _code.getPosition(line, column);
}
void mousedown(MouseEvent e) {
// for shift click, create selection region
if (e.shiftKey) {
_cursor._toPos = _cursor._pos;
} else {
_cursor.clearSelection();
}
_cursor._pos = _code.positionFromMouse(e);
focus();
e.preventDefault();
_isSelecting = true;
}
void mousemove(MouseEvent e) {
// TODO(jimhug): Would REALLY like to check that button is down here!
if (_isSelecting) {
if (_cursor._toPos == null) {
_cursor._toPos = _cursor._pos;
}
_cursor._pos = _code.positionFromMouse(e);
e.preventDefault();
_redraw();
}
}
void mouseup(MouseEvent e) {
_isSelecting = false;
if (_cursor.emptySelection) {
_cursor.clearSelection();
}
}
void scrollToVisible(CodePosition pos) {
var top = _node.scrollTop;
var height = _node.getBoundingClientRect().height;
var pt = pos.getPoint();
if (pt.y < top) {
_node.scrollTop = pt.y;
} else if (pt.y > top + height - LINE_HEIGHT) {
var H = LINE_HEIGHT * ((height ~/ LINE_HEIGHT) - 1);
_node.scrollTop = Math.max(pt.y - H, 0);
}
}
void _redraw() {
scrollToVisible(_cursor._pos);
_code._redraw();
_cursor._redraw();
}
void focus() {
_shell.focusKeys(this);
_cursor._visible = true;
_redraw();
}
void blur() {
_shell.blurKeys(this);
_cursor._visible = false;
_redraw();
}
}
class Point {
final int x, y;
const Point(this.x, this.y);
}
class LineColumn {
final int line, column;
LineColumn(this.line, this.column);
}
class Cursor {
CodePosition _pos;
CodePosition _toPos;
bool _visible = true;
var _node;
var _cursorNode;
var _selectionNode;
Cursor(this._pos) {
_node = document.createElement('div');
_node.className = 'cursorDiv';
}
bool get emptySelection() {
return _toPos == null ||
(_toPos.block == _pos.block && _toPos.offset == _pos.offset);
}
_redraw() {
// Approach is to kill and recreate everything on a redraw.
// There are lots of potential improvements if this proves costly.
// However: If we don't do this we need a different dance to make cursor
// blinking disabled when it is moving.
_node.innerHTML = '';
if (!_visible) return;
_cursorNode = document.createElement('div');
_cursorNode.className = 'cursor blink';
_cursorNode.style.setProperty('height', '${LINE_HEIGHT}px');
var p = _pos.getPoint();
_cursorNode.style.setProperty('left', '${p.x}px');
_cursorNode.style.setProperty('top', '${p.y}px');
_node.appendChild(_cursorNode);
if (_toPos == null) return;
void addDiv(top, left, height, width) {
var child = document.createElement('div');
child.className = 'selection';
child.style.setProperty('left', '${left}px');
child.style.setProperty('top', '${top}px');
child.style.setProperty('height', '${height}px');
if (width == null) {
child.style.setProperty('right', '0px');
} else {
child.style.setProperty('width', '${width}px');
}
_node.appendChild(child);
}
var toP = _toPos.getPoint();
// Same line - only one line to highlight
if (toP.y == p.y) {
if (toP.x < p.x) {
addDiv(p.y, toP.x, LINE_HEIGHT, p.x - toP.x);
} else {
addDiv(p.y, p.x, LINE_HEIGHT, toP.x - p.x);
}
} else {
if (toP.y < p.y) {
var tmp = toP; toP = p; p = tmp;
}
addDiv(p.y, p.x, LINE_HEIGHT, null);
if (toP.y > p.y + LINE_HEIGHT) {
addDiv(p.y + LINE_HEIGHT, 0, toP.y - p.y - LINE_HEIGHT, null);
}
addDiv(toP.y, 0, LINE_HEIGHT, toP.x);
}
// TODO(jimhug): separate out - this makes default copy/cut work
var p0 = _pos.toRoot();
var p1 = _toPos.toRoot();
var i0 = p0.offset;
var i1 = p1.offset;
if (i1 < i0) {
var tmp = i1; i1 = i0; i0 = tmp;
}
var text = p0.block.text.substring(i0, i1);
shell._textInputArea.value = text;
shell._textInputArea.select();
}
void clearSelection() {
_toPos = null;
}
void moveColumn(int delta) {
_pos = _pos.moveColumn(delta);
}
void moveLine(int delta) {
_pos = _pos.moveLine(delta);
}
void deleteSelection() {
if (_toPos == null) return;
var p0 = _pos;
var p1 = _toPos;
if (p0.block != p1.block) {
// move up to root and resolve there...
p0 = p0.toRoot();
p1 = p1.toRoot();
}
assert(p0.block == p1.block);
if (p1.offset < p0.offset) {
var tmp = p0; p0 = p1; p1 = tmp;
}
p0.block.delete(p0.offset, p1.offset);
_pos = p0.toLeaf();
_toPos = null;
}
void write(String text) {
// TODO(jimhug): combine insert and delete to optimize
deleteSelection();
_pos.block.insertText(_pos.offset, text);
_pos.block._redraw(); // TODO(jimhug): Egregious hack.
_pos = _pos.moveColumn(text.length);
}
}
class CodePosition {
final CodeBlock block;
final int offset;
CodePosition(this.block, this.offset);
CodePosition moveLine(int delta) {
if (delta == 0) return this;
var lineCol = getLineColumn();
return block.getPosition(lineCol.line + delta, lineCol.column);
}
CodePosition moveColumn(int delta) {
return block.moveToOffset(offset + delta);
}
Point getPoint() {
return block.offsetToPoint(offset);
}
LineColumn getLineColumn() {
return block.getLineColumn(offset);
}
CodePosition toRoot() {
if (block._parent === null) return this;
var ret = new CodePosition(block._parent,
block._parent.getOffset(block) + offset);
return ret.toRoot();
}
CodePosition toLeaf() {
return block.moveToOffset(offset);
}
}
/**
* Every [CodeBlock] must provide the following:
* - a node to render it - purely as vertical div
* - a height in real pixels and in code lines
* - proper interaction with CodePosition
* - accurate and working get/set for text property
* - appropriate integration into parsing
* - support to hold onto annotations of various sorts
*/
class BlockChildren implements Iterable<CodeBlock> {
BlockBlock _parent;
BlockChildren(this._parent);
Iterator<CodeBlock> iterator() {
return new BlockChildrenIterator(_parent._firstChild);
}
}
class BlockChildrenIterator implements Iterator<CodeBlock> {
CodeBlock _child;
BlockChildrenIterator(this._child);
// TODO(jimhug): current + moveNext() is much more sane.
CodeBlock next() {
var ret = _child;
_child = _child._nextSibling;
return ret;
}
bool hasNext() => _child !== null;
}
/**
* Block block... first and last children are special except for top file.
*/
class BlockBlock extends CodeBlock {
CodeBlock _firstChild;
CodeBlock _lastChild;
Iterable<CodeBlock> children;
BlockBlock(CodeBlock parent, int depth): super(parent, depth) {
text = '';
children = new BlockChildren(this);
}
CodePosition get start() => _firstChild.start;
CodePosition get end() => _lastChild.end;
int get size() {
var ret = 0;
for (var child in children) ret += child.size;
return ret;
}
int get lineCount() {
var ret = 0;
for (var child in children) ret += child.lineCount;
return ret;
}
// TODO(jimhug): This property is expensive here - should it be a prop?
String get text() {
var ret = new StringBuffer();
for (var child in children) ret.add(child.text);
return ret.toString();
}
int getOffset(CodeBlock forChild) {
var ret = 0;
for (var child in children) {
if (child == forChild) return ret;
ret += child.size;
}
throw "child missing";
}
int getLine(CodeBlock forChild) {
var ret = 0;
for (var child in children) {
if (child == forChild) return ret;
ret += child.lineCount;
}
throw "child missing";
}
_addChildAfter(CodeBlock addAfterChild, CodeBlock child) {
markDirty();
child._nextSibling = addAfterChild._nextSibling;
child._previousSibling = addAfterChild;
addAfterChild._nextSibling = child;
// Add child's node into our DOM tree.
if (child._nextSibling === null) {
_node.appendChild(child._node);
_lastChild = child;
} else {
_node.insertBefore(child._node, child._nextSibling._node);
}
}
_removeChild(CodeBlock child) {
markDirty();
if (child._previousSibling !== null) {
child._previousSibling._nextSibling = child._nextSibling;
} else {
_firstChild = child._nextSibling;
}
if (child._nextSibling !== null) {
child._nextSibling._previousSibling = child._previousSibling;
} else {
_lastChild = child._previousSibling;
}
// Remove child's node from our DOM tree.
_node.removeChild(child._node);
}
void set text(String newText) {
final sw = new Stopwatch();
sw.start();
_firstChild = _lastChild = null;
final src = new SourceFile('fake.dart', newText);
int start = 0;
while (start != -1) {
var child = new TextBlock(this, null, _depth + 1);
if (_lastChild === null) {
_firstChild = _lastChild = child;
_node.appendChild(child._node);
} else {
_addChildAfter(_lastChild, child);
}
start = child.tokenizeInto(src, start);
}
sw.stop();
print('create structure in ${sw.elapsedInMs()}msec');
}
void insertText(int offset, String newText) {
var index = 0;
for (var child in children) {
var childSize = child.size;
if (offset < childSize) {
child.insertText(offset, newText);
return;
} else if (offset == childSize) {
// TODO(jimhug): Nasty merging of text and block structure here.
var newChild = new TextBlock(this, newText, _depth + 1);
_addChildAfter(child, newChild);
return;
}
offset -= childSize;
index++;
}
// TODO: nesting at this level
throw "help";
}
void delete(int from, int to) {
assert(from <= to);
var keepChild = null;
for (var child = _firstChild; child !== null; child = child._nextSibling) {
var childSize = child.size;
if (keepChild !== null) {
_removeChild(child);
if (to <= childSize) {
// abstraction violation!!!
keepChild._text += child._text.substring(to);
return;
}
} else if (from <= childSize) {
if (to < childSize) {
child.delete(from, to);
return;
} else {
child.delete(from, childSize);
keepChild = child;
}
}
from -= childSize;
to -= childSize;
}
// TODO: nesting at this level
throw "help";
}
_redraw() {
if (!_dirty) return;
_dirty = false;
var childTop = 0;
for (var child = _firstChild; child !== null; child = child._nextSibling) {
// Note: Performance here relies on lazy setter in CodeBlock.
child.top = childTop;
child._redraw();
childTop += child.height;
}
height = childTop;
}
CodePosition moveToOffset(int offset) {
for (var child in children) {
var childSize = child.size;
if (offset < childSize) {
return child.moveToOffset(offset);
}
offset -= childSize;
}
// TODO: nesting at this level
return end;
}
CodePosition positionFromPoint(int x, int y) {
if (y < top) return start;
for (var child in children) {
if (child.top <= y && (child.top + child.height) >= y) {
return child.positionFromPoint(x, y - child.top);
}
}
// TODO: next level of nesting...
return end;
}
CodePosition getPosition(int line, int column) {
if (line < 0) return start;
for (var child in children) {
if (line < child.lineCount) return child.getPosition(line, column);
line -= child.lineCount;
}
return end; // TODO
}
// These are local line/column to this block??????
LineColumn getLineColumn(int offset) {
if (offset < 0) return new LineColumn(0, 0);
int childLine = 0;
for (var child in children) {
var childSize = child.size;
if (offset < childSize) {
// TODO: This needs modification!!!
var ret = child.getLineColumn(offset);
return new LineColumn(ret.line + childLine, ret.column);
}
offset -= childSize;
childLine += child.lineCount;
}
// TODO: nesting at this level
return new LineColumn(lineCount, 0); // ??? wrong end column
}
Point offsetToPoint(int offset) {
if (offset < 0) return new Point(0, 0);
for (var child in children) {
var childSize = child.size;
if (offset < childSize) {
var ret = child.offsetToPoint(offset);
return new Point(ret.x, ret.y + child.top);
}
offset -= childSize;
}
// TODO: nesting at this level
return new Point(0, top + height);
}
}
/**
* Pure text block - terminals.
*/
class TextBlock extends CodeBlock {
List<int> _lineStarts;
String _text;
TextBlock(CodeBlock parent, this._text, int depth): super(parent, depth) {
//window.console.warn('tb: "$_text"');
}
int get size() => _text.length;
int get lineCount() => _lineStarts.length;
String get text() => _text;
void set text(String newText) {
_text = newText;
markDirty();
}
void insertText(int offset, String newText) {
_text = _text.substring(0, offset) + newText + _text.substring(offset);
markDirty();
}
void delete(int from, int to) {
assert(from <= to);
assert(to <= _text.length);
markDirty();
if (to == _text.length) {
if (from == 0) {
_parent._removeChild(this);
} else {
_text = _text.substring(0, from);
}
} else {
_text = _text.substring(0, from) + _text.substring(to);
}
}
int tokenizeInto(SourceFile src, int start) {
_lineStarts = new List<int>();
_lineStarts.add(start);
// classify my text and create siblings and parents as needed
var html = new StringBuffer();
Tokenizer tokenizer = new Tokenizer(src, /*skipWhitespace:*/false, start);
int depth = 0;
// TODO(jimhug): REALLY INEFFICIENT!
void addLineStarts(Token token) {
// TODO(jimhug): Should we just make the Tokenizer do this directly?
final text = src.text;
for (int index = token.start; index < token.end; index++) {
if (text.charCodeAt(index) == 10/*'\n'*/) {
_lineStarts.add(index - start + 1);
}
}
}
// TODO(jimhug): Add whitespace blocks?
while (true) {
var token = tokenizer.next();
if (token.kind == TokenKind.END_OF_FILE) {
_node.innerHTML = html.toString();
if (start == 0) _text = src.text;
else _text = src.text.substring(start);
height = lineCount * LINE_HEIGHT;
// update parent
return -1;
} else if (token.kind == TokenKind.WHITESPACE) {
// TODO(jimhug): Special handling for pure whitespace divs?
if (src.text.charCodeAt(token.end-1) == 10/*'\n'*/) {
// Model 1 - create siblings at any "true" line break
// -- set my from source
_text = src.text.substring(start, token.end);
_node.innerHTML = html.toString();
height = lineCount * LINE_HEIGHT;
// update parent...
return token.end;
}
} else if (token.kind == TokenKind.COMMENT) {
// TODO(jimhug): These may be the most fun blocks to handle...
addLineStarts(token);
} else if (token.kind == TokenKind.STRING) {
addLineStarts(token);
} else if (token.kind == TokenKind.STRING_PART) {
addLineStarts(token);
}
final kind = classify(token);
final stringClass = '';
final text = htmlEscape(token.text);
if (kind != null) {
html.add('<span class="$kind $stringClass">$text</span>');
} else {
html.add('<span>$text</span>');
}
}
}
_redraw() {
if (!_dirty) return;
_dirty = false;
var initialText = _text;
var end = tokenizeInto(new SourceFile('fake.dart', _text), 0);
if (_text.length < initialText.length) {
// ??? How to know we want a new block ???
var extraText = initialText.substring(_text.length);
_parent.insertText(_parent.getOffset(this) + _text.length, extraText);
}
}
CodePosition moveToOffset(int offset) {
if (offset < 0 || offset >= _text.length) {
return _parent.moveToOffset(_parent.getOffset(this) + offset);
}
return new CodePosition(this, offset);
}
CodePosition positionFromPoint(int x, int y) {
return getPosition((y / LINE_HEIGHT).floor(), (x / CHAR_WIDTH).round());
}
CodePosition getPosition(int line, int column) {
if (line < 0 || line >= lineCount) {
return _parent.getPosition(_parent.getLine(this) + line, column);
}
int maxOffset;
if (line < _lineStarts.length - 1) {
maxOffset = _lineStarts[line + 1] - 1;
} else {
maxOffset = _text.length - 1;
}
final offset = Math.min(_lineStarts[line] + column, maxOffset);
return new CodePosition(this, offset);
}
// These are local line/column to this block
LineColumn getLineColumn(int offset) {
if (_lineStarts === null) {
return new LineColumn(0, 0);
}
// TODO(jimhug): Binary search would be faster but more complicated.
int previousStart = 0;
int line = 1;
for (; line < _lineStarts.length; line++) {
int start = _lineStarts[line];
if (start > offset) {
break;
}
previousStart = start;
}
return new LineColumn(line - 1, offset - previousStart);
}
Point offsetToPoint(int offset) {
LineColumn lc = getLineColumn(offset);
return new Point(lc.column * CHAR_WIDTH, top + (lc.line * LINE_HEIGHT));
}
}
class CodeBlock {
CodeBlock _parent;
CodeBlock _previousSibling;
CodeBlock _nextSibling;
int _depth = 0;
bool _dirty = true;
var _node;
int _top, _height;
CodeBlock(this._parent, this._depth) {
_node = document.createElement('div');
_node.className = 'code'; // TODO - different kinds of nodes
}
abstract int size();
abstract int get lineCount();
abstract String get text();
abstract void set text(String newText);
abstract CodePosition moveToOffset(int offset);
CodePosition get start() => new CodePosition(this, 0);
CodePosition get end() => new CodePosition(this, size);
int getOffset(CodeBlock forChild) {
throw "child missing";
}
int getLine(CodeBlock forChild) {
throw "child missing";
}
_removeChild(CodeBlock child) {
throw "child missing";
}
void parse() {
final source = new SourceFile('fake.dart', text);
var p = new Parser(source);
var cu = p.compilationUnit();
}
void markDirty() {
if (!_dirty) {
_dirty = true;
if (_parent != null) {
_parent._dirty = true;
}
}
}
int get top() => _top;
void set top(int newTop) {
if (newTop != _top) {
_top = newTop;
_node.style.setProperty('top', '${_top}px');
}
}
int get height() => _height;
void set height(int newHeight) {
if (newHeight != _height) {
_height = newHeight;
_node.style.setProperty('height', '${_height}px');
}
}
abstract void insertText(int offset, String newText);
abstract void delete(int from, int to);
CodePosition positionFromMouse(MouseEvent p) {
var box = _node.getBoundingClientRect();
int y = p.clientY - box.top;
int x = p.clientX - box.left;
return positionFromPoint(x, y);
}
abstract CodePosition positionFromPoint(int x, int y);
abstract CodePosition getPosition(int line, int column);
// These are local line/column to this block
abstract LineColumn getLineColumn(int offset);
abstract Point offsetToPoint(int offset);
abstract void _redraw();
}
class KeyBindings {
static final Map _remap = const {
'U+001B':'Esc', 'U+0008':'Delete', 'U+0009':'Tab', 'U+0020':'Space',
'Shift':'', 'Control':'', 'Alt':'', 'Meta':''
};
static String _getModifiers(event) {
String ret = '';
if (event.ctrlKey) { ret += 'Control-'; }
if (event.altKey) { ret += 'Alt-'; }
if (event.metaKey) { ret += 'Meta-'; }
if (event.shiftKey) { ret += 'Shift-'; }
return ret;
}
// TODO(jimhug): Move this to base <= 36 and into shared code.
static int _hexDigit(int c) {
if(c >= 48/*0*/ && c <= 57/*9*/) {
return c - 48;
} else if (c >= 97/*a*/ && c <= 102/*f*/) {
return c - 87;
} else if (c >= 65/*A*/ && c <= 70/*F*/) {
return c - 55;
} else {
return -1;
}
}
static int parseHex(String hex) {
var result = 0;
for (int i=0; i < hex.length; i++) {
var digit = _hexDigit(hex.charCodeAt(i));
assert(digit != -1);
result = (result << 4) + digit;
}
return result;
}
static String translate(event) {
var ret = _remap[event.keyIdentifier];
if (ret === null) ret = event.keyIdentifier;
if (ret == '') {
return null;
} else if (ret.startsWith('U+')) {
// This method only reports "non-text" key presses
if (event.ctrlKey || event.altKey || event.metaKey) {
return _getModifiers(event) +
new String.fromCharCodes([parseHex(ret.substring(2, ret.length))]);
} else {
return null;
}
} else {
return _getModifiers(event) + ret;
}
}
var node;
Map bindings;
var handleText, handleUnknown;
KeyBindings(this.node, this.bindings, this.handleText, this.handleUnknown) {
node.addEventListener('textInput', onTextInput, false);
node.addEventListener('keydown', onKeydown, false);
}
onTextInput(TextEvent event) {
var text = event.data;
var ret;
if (bindings[text] !== null) {
ret = bindings[text]();
} else {
ret = handleText(text);
}
// TODO(jimhug): Unfortunate coupling to shell.
shell._editor._redraw();
return ret;
}
// TODO(jimhug): KeyboardEvent type is needed!
onKeydown(KeyboardEvent event) {
final key = translate(event);
if (key !== null) {
if (bindings[key] !== null) {
bindings[key]();
event.preventDefault();
} else {
if (handleUnknown(key)) {
event.preventDefault();
} else {
event.stopPropagation();
}
}
} else {
event.stopPropagation();
}
// TODO(jimhug): Unfortunate coupling to shell.
shell._editor._redraw();
return false;
}
}
// TODO(jimhug): Copy, paste and then modified from dartdoc
/**
* Kinds of tokens that we care to highlight differently. The values of the
* fields here will be used as CSS class names for the generated spans.
*/
class Classification {
static final NONE = null;
static final ERROR = "e";
static final COMMENT = "c";
static final IDENTIFIER = "i";
static final KEYWORD = "k";
static final OPERATOR = "o";
static final STRING = "s";
static final NUMBER = "n";
static final PUNCTUATION = "p";
// A few things that are nice to make different:
static final TYPE_IDENTIFIER = "t";
// Between a keyword and an identifier
static final SPECIAL_IDENTIFIER = "r";
static final ARROW_OPERATOR = "a";
static final STRING_INTERPOLATION = 'si';
}
// TODO(rnystrom): should exist in standard lib somewhere
String htmlEscape(String text) {
return text.replaceAll('&', '&amp;').replaceAll(
'>', '&gt;').replaceAll('<', '&lt;');
}
bool _looksLikeType(String name) {
// If the name looks like an UppercaseName, assume it's a type.
return _looksLikePublicType(name) || _looksLikePrivateType(name);
}
bool _looksLikePublicType(String name) {
// If the name looks like an UppercaseName, assume it's a type.
return name.length >= 2 && isUpper(name[0]) && isLower(name[1]);
}
bool _looksLikePrivateType(String name) {
// If the name looks like an _UppercaseName, assume it's a type.
return (name.length >= 3 && name[0] == '_' && isUpper(name[1])
&& isLower(name[2]));
}
// These ensure that they don't return "true" if the string only has symbols.
bool isUpper(String s) => s.toLowerCase() != s;
bool isLower(String s) => s.toUpperCase() != s;
String classify(Token token) {
switch (token.kind) {
case TokenKind.ERROR:
return Classification.ERROR;
case TokenKind.IDENTIFIER:
// Special case for names that look like types.
if (_looksLikeType(token.text)
|| token.text == 'num'
|| token.text == 'bool'
|| token.text == 'int'
|| token.text == 'double') {
return Classification.TYPE_IDENTIFIER;
}
return Classification.IDENTIFIER;
// Even though it's a reserved word, let's try coloring it like a type.
case TokenKind.VOID:
return Classification.TYPE_IDENTIFIER;
case TokenKind.THIS:
case TokenKind.SUPER:
return Classification.SPECIAL_IDENTIFIER;
case TokenKind.STRING:
case TokenKind.STRING_PART:
case TokenKind.INCOMPLETE_STRING:
case TokenKind.INCOMPLETE_MULTILINE_STRING_DQ:
case TokenKind.INCOMPLETE_MULTILINE_STRING_SQ:
return Classification.STRING;
case TokenKind.INTEGER:
case TokenKind.HEX_INTEGER:
case TokenKind.DOUBLE:
return Classification.NUMBER;
case TokenKind.COMMENT:
case TokenKind.INCOMPLETE_COMMENT:
return Classification.COMMENT;
// => is so awesome it is in a class of its own.
case TokenKind.ARROW:
return Classification.ARROW_OPERATOR;
case TokenKind.HASHBANG:
case TokenKind.LPAREN:
case TokenKind.RPAREN:
case TokenKind.LBRACK:
case TokenKind.RBRACK:
case TokenKind.LBRACE:
case TokenKind.RBRACE:
case TokenKind.COLON:
case TokenKind.SEMICOLON:
case TokenKind.COMMA:
case TokenKind.DOT:
case TokenKind.ELLIPSIS:
return Classification.PUNCTUATION;
case TokenKind.INCR:
case TokenKind.DECR:
case TokenKind.BIT_NOT:
case TokenKind.NOT:
case TokenKind.ASSIGN:
case TokenKind.ASSIGN_OR:
case TokenKind.ASSIGN_XOR:
case TokenKind.ASSIGN_AND:
case TokenKind.ASSIGN_SHL:
case TokenKind.ASSIGN_SAR:
case TokenKind.ASSIGN_SHR:
case TokenKind.ASSIGN_ADD:
case TokenKind.ASSIGN_SUB:
case TokenKind.ASSIGN_MUL:
case TokenKind.ASSIGN_DIV:
case TokenKind.ASSIGN_TRUNCDIV:
case TokenKind.ASSIGN_MOD:
case TokenKind.CONDITIONAL:
case TokenKind.OR:
case TokenKind.AND:
case TokenKind.BIT_OR:
case TokenKind.BIT_XOR:
case TokenKind.BIT_AND:
case TokenKind.SHL:
case TokenKind.SAR:
case TokenKind.SHR:
case TokenKind.ADD:
case TokenKind.SUB:
case TokenKind.MUL:
case TokenKind.DIV:
case TokenKind.TRUNCDIV:
case TokenKind.MOD:
case TokenKind.EQ:
case TokenKind.NE:
case TokenKind.EQ_STRICT:
case TokenKind.NE_STRICT:
case TokenKind.LT:
case TokenKind.GT:
case TokenKind.LTE:
case TokenKind.GTE:
case TokenKind.INDEX:
case TokenKind.SETINDEX:
return Classification.OPERATOR;
// Color this like a keyword
case TokenKind.HASH:
case TokenKind.ABSTRACT:
case TokenKind.ASSERT:
case TokenKind.CLASS:
case TokenKind.EXTENDS:
case TokenKind.FACTORY:
case TokenKind.GET:
case TokenKind.IMPLEMENTS:
case TokenKind.IMPORT:
case TokenKind.INTERFACE:
case TokenKind.LIBRARY:
case TokenKind.NATIVE:
case TokenKind.NEGATE:
case TokenKind.OPERATOR:
case TokenKind.SET:
case TokenKind.SOURCE:
case TokenKind.STATIC:
case TokenKind.TYPEDEF:
case TokenKind.BREAK:
case TokenKind.CASE:
case TokenKind.CATCH:
case TokenKind.CONST:
case TokenKind.CONTINUE:
case TokenKind.DEFAULT:
case TokenKind.DO:
case TokenKind.ELSE:
case TokenKind.FALSE:
case TokenKind.FINALLY:
case TokenKind.FOR:
case TokenKind.IF:
case TokenKind.IN:
case TokenKind.IS:
case TokenKind.NEW:
case TokenKind.NULL:
case TokenKind.RETURN:
case TokenKind.SWITCH:
case TokenKind.THROW:
case TokenKind.TRUE:
case TokenKind.TRY:
case TokenKind.WHILE:
case TokenKind.VAR:
case TokenKind.FINAL:
return Classification.KEYWORD;
case TokenKind.WHITESPACE:
case TokenKind.END_OF_FILE:
return Classification.NONE;
default:
return Classification.NONE;
}
}