// 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 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 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 = '
REPL Under Construction...
'; _node.appendChild(_repl); _errors = new MessageWindow(); //_errors.innerHTML = '

Errors/Warnings Under Construction...

'; _errors._node.style.setProperty('height', '15%'); _node.appendChild(_errors._node); // TODO(jimhug): Ick! window.setTimeout( () { _editor.focus(); _output.contentDocument.head.innerHTML = ''' '''; _output.contentDocument.body.innerHTML = '

Output will go here

'; }, .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 = '

Compiling...

'; _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 = '

Compilation failed

'; } 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 { BlockBlock _parent; BlockChildren(this._parent); Iterator iterator() { return new BlockChildrenIterator(_parent._firstChild); } } class BlockChildrenIterator implements Iterator { 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 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 _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(); _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('$text'); } else { html.add('$text'); } } } _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('&', '&').replaceAll( '>', '>').replaceAll('<', '<'); } 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; } }