| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file | |
| 2 // for details. All rights reserved. Use of this source code is governed by a | |
| 3 // BSD-style license that can be found in the LICENSE file. | |
| 4 | |
| 5 /** | |
| 6 * This giant file has pieces for compiling dart in the browser, injecting | |
| 7 * scripts into pages and a bunch of classes to build a simple dart editor. | |
| 8 * | |
| 9 * TODO(jimhug): Separate these pieces cleanly. | |
| 10 */ | |
| 11 | |
| 12 #import('dart:dom'); | |
| 13 #import('../lang.dart'); | |
| 14 #import('../file_system_dom.dart'); | |
| 15 | |
| 16 | |
| 17 void main() { | |
| 18 final systemPath = getRootPath(window) + '/..'; | |
| 19 final userPath = getRootPath(window.parent); | |
| 20 | |
| 21 if (window !== window.parent) { | |
| 22 // I'm in an iframe - frogify my surrounding code unless dart is native. | |
| 23 if (!document.implementation.hasFeature('dart', '')) { | |
| 24 // Suppress warnings to avoid diff-based tests from failing on them. | |
| 25 initialize(systemPath, userPath, ['--suppress_warnings']); | |
| 26 window.addEventListener('DOMContentLoaded', | |
| 27 (e) => frogify(window.parent), | |
| 28 false); | |
| 29 } | |
| 30 } else { | |
| 31 // I'm at the top level - run the tip shell. | |
| 32 shell = new Shell(); | |
| 33 document.body.appendChild(shell._node); | |
| 34 initialize(systemPath, userPath); | |
| 35 world.messageHandler = shell.handleMessage; | |
| 36 } | |
| 37 } | |
| 38 | |
| 39 | |
| 40 /** The filename to use for Dart code compiled directly from the browser. */ | |
| 41 final String DART_FILENAME = 'input.dart'; | |
| 42 | |
| 43 /** Helper to get my base url. */ | |
| 44 String getRootPath(Window window) { | |
| 45 String url = window.location.href; | |
| 46 final tail = url.lastIndexOf('/', url.length); | |
| 47 final dir = url.substring(0, tail); | |
| 48 return dir; | |
| 49 } | |
| 50 | |
| 51 | |
| 52 /** | |
| 53 * Invoke this script in the given window's context. Use | |
| 54 * this frame's window by default. | |
| 55 */ | |
| 56 void inject(String code, Window win, [String name = 'generated.js']) { | |
| 57 final doc = win.document; | |
| 58 var script = doc.createElement('script'); | |
| 59 // TODO(vsm): Enable debugging of injected code. This sourceURL | |
| 60 // trick only appears to work for eval'ed code, not script injected | |
| 61 // code. | |
| 62 // Append sourceURL to enable debugging. | |
| 63 script.innerHTML = code + '\n//@ sourceURL=$name'; | |
| 64 script.type = 'application/javascript'; | |
| 65 doc.body.appendChild(script); | |
| 66 } | |
| 67 | |
| 68 | |
| 69 /** | |
| 70 * Compile all dart scripts in a window and inject/invoke the corresponding JS. | |
| 71 */ | |
| 72 void frogify(Window win) { | |
| 73 var doc = win.document; | |
| 74 int n = doc.scripts.length; | |
| 75 // TODO(vsm): Implement foreach iteration on native DOM types. This | |
| 76 // should be for (var script in doc.scripts) { ... }. | |
| 77 for (int i = 0; i < n; ++i) { | |
| 78 final script = doc.scripts[i]; | |
| 79 if (script.type == 'application/dart') { | |
| 80 final src = script.src; | |
| 81 var input; | |
| 82 var name; | |
| 83 if (src == '') { | |
| 84 input = script.innerHTML; | |
| 85 name = null; | |
| 86 } else { | |
| 87 input = world.files.readAll(src); | |
| 88 name = '$src.js'; | |
| 89 } | |
| 90 world.files.writeString(DART_FILENAME, input); | |
| 91 world.reset(); | |
| 92 var success = world.compile(); | |
| 93 | |
| 94 if (success) { | |
| 95 inject(world.getGeneratedCode(), win, name); | |
| 96 } else { | |
| 97 inject('window.alert("compilation failed");', win, name); | |
| 98 } | |
| 99 } | |
| 100 } | |
| 101 } | |
| 102 | |
| 103 void initialize(String systemPath, String userPath, | |
| 104 [List<String> flags = const []]) { | |
| 105 DomFileSystem fs = new DomFileSystem(userPath); | |
| 106 // TODO(jimhug): Workaround lib path hack in frog_options.dart | |
| 107 final options = [null, null, '--libdir=$systemPath/lib']; | |
| 108 options.addAll(flags); | |
| 109 options.add(DART_FILENAME); | |
| 110 parseOptions(systemPath, options, fs); | |
| 111 initializeWorld(fs); | |
| 112 } | |
| 113 | |
| 114 final int LINE_HEIGHT = 22; // TODO(jimhug): This constant sucks. | |
| 115 final int CHAR_WIDTH = 8; // TODO(jimhug): See above. | |
| 116 | |
| 117 final String CODE = ''' | |
| 118 #import("dart:dom"); | |
| 119 | |
| 120 // This is an interesting field; | |
| 121 final int y = 22; | |
| 122 String name; | |
| 123 | |
| 124 /** This is my main method. */ | |
| 125 void main() { | |
| 126 var element = document.createElement('div'); | |
| 127 element.innerHTML = "Hello dom from Dart!"; | |
| 128 document.body.appendChild(element); | |
| 129 | |
| 130 HTMLCanvasElement canvas = document.createElement('canvas'); | |
| 131 document.body.appendChild(canvas); | |
| 132 | |
| 133 var context = canvas.getContext('2d'); | |
| 134 context.setFillColor('purple'); | |
| 135 context.fillRect(10, 10, 30, 30); | |
| 136 } | |
| 137 | |
| 138 /** | |
| 139 * The usual method of computing factorial in the slowest possible way. | |
| 140 */ | |
| 141 num fact(n) { | |
| 142 if (n == 0) return 1; | |
| 143 return n * fact(n - 1); | |
| 144 } | |
| 145 | |
| 146 final x = 22; | |
| 147 | |
| 148 '''; | |
| 149 | |
| 150 final String HCODE = '''#import("dart:html"); | |
| 151 | |
| 152 void main() { | |
| 153 var element = document.createElement('div'); | |
| 154 element.innerHTML = "Hello html from Dart!"; | |
| 155 document.body.nodes.add(element); | |
| 156 | |
| 157 CanvasElement canvas = document.createElement('canvas'); | |
| 158 canvas.width = 100; | |
| 159 canvas.height = 100; | |
| 160 document.body.nodes.add(canvas); | |
| 161 | |
| 162 var context = canvas.getContext('2d'); | |
| 163 context.setFillColor('blue'); | |
| 164 context.fillRect(10, 10, 30, 30); | |
| 165 } | |
| 166 '''; | |
| 167 | |
| 168 | |
| 169 class Message { | |
| 170 var _node; | |
| 171 String prefix, message; | |
| 172 SourceSpan span; | |
| 173 | |
| 174 Message(this.prefix, this.message, this.span) { | |
| 175 var col = prefix.indexOf(':'); | |
| 176 if (col != -1) { | |
| 177 prefix = prefix.substring(0, col); | |
| 178 } | |
| 179 | |
| 180 _node = document.createElement('div'); | |
| 181 _node.className = 'message ${prefix}'; | |
| 182 _node.innerText = '$message at ${span.locationText}'; | |
| 183 | |
| 184 _node.addEventListener('click', click, false); | |
| 185 } | |
| 186 | |
| 187 void click(MouseEvent event) { | |
| 188 shell.showSpan(span); | |
| 189 } | |
| 190 } | |
| 191 | |
| 192 class MessageWindow { | |
| 193 var _node; | |
| 194 List<Message> messages; | |
| 195 | |
| 196 MessageWindow() { | |
| 197 messages = []; | |
| 198 _node = document.createElement('div'); | |
| 199 _node.className = 'errors'; | |
| 200 } | |
| 201 | |
| 202 addMessage(Message message) { | |
| 203 messages.add(message); | |
| 204 _node.appendChild(message._node); | |
| 205 } | |
| 206 | |
| 207 clear() { | |
| 208 messages.length = 0; | |
| 209 _node.innerHTML = ''; | |
| 210 } | |
| 211 } | |
| 212 | |
| 213 | |
| 214 // TODO(jimhug): Type is needed. | |
| 215 Shell shell; | |
| 216 | |
| 217 class Shell { | |
| 218 var _textInputArea; | |
| 219 KeyBindings _bindings; | |
| 220 | |
| 221 Cursor cursor; | |
| 222 Editor _editor; | |
| 223 | |
| 224 var _node; | |
| 225 var _output; | |
| 226 | |
| 227 var _repl; | |
| 228 var _errors; | |
| 229 | |
| 230 Shell() { | |
| 231 _node = document.createElement('div'); | |
| 232 _node.className = 'shell'; | |
| 233 _editor = new Editor(this); | |
| 234 | |
| 235 _editor._node.style.setProperty('height', '93%'); | |
| 236 _node.appendChild(_editor._node); | |
| 237 | |
| 238 _textInputArea = document.createElement('textarea'); | |
| 239 _textInputArea.className = 'hiddenTextArea'; | |
| 240 | |
| 241 _node.appendChild(_textInputArea); | |
| 242 | |
| 243 var outDiv = document.createElement('div'); | |
| 244 outDiv.className = 'output'; | |
| 245 outDiv.style.setProperty('height', '83%'); | |
| 246 | |
| 247 | |
| 248 _output = document.createElement('iframe'); | |
| 249 outDiv.appendChild(_output); | |
| 250 _node.appendChild(outDiv); | |
| 251 | |
| 252 _repl = document.createElement('div'); | |
| 253 _repl.className = 'repl'; | |
| 254 _repl.style.setProperty('height', '5%'); | |
| 255 _repl.innerHTML = '<div>REPL Under Construction...</div>'; | |
| 256 _node.appendChild(_repl); | |
| 257 | |
| 258 _errors = new MessageWindow(); | |
| 259 | |
| 260 //_errors.innerHTML = '<h3>Errors/Warnings Under Construction...</h3>'; | |
| 261 _errors._node.style.setProperty('height', '15%'); | |
| 262 _node.appendChild(_errors._node); | |
| 263 | |
| 264 // TODO(jimhug): Ick! | |
| 265 window.setTimeout( () { | |
| 266 _editor.focus(); | |
| 267 _output.contentDocument.head.innerHTML = ''' | |
| 268 <style>body { | |
| 269 font-family: arial , sans-serif; | |
| 270 } | |
| 271 h3 { | |
| 272 text-align: center; | |
| 273 } | |
| 274 </style>'''; | |
| 275 _output.contentDocument.body.innerHTML = '<h3>Output will go here</h3>'; | |
| 276 }, .5); | |
| 277 | |
| 278 // TODO(jimhug): These are hugely incomplete and Mac-centric. | |
| 279 var bindings = { | |
| 280 'Left': () { | |
| 281 cursor.clearSelection(); | |
| 282 cursor._pos = cursor._pos.moveColumn(-1); | |
| 283 }, | |
| 284 'Shift-Left': () { | |
| 285 if (cursor._toPos == null) { | |
| 286 cursor._toPos = cursor._pos; | |
| 287 } | |
| 288 cursor._pos = cursor._pos.moveColumn(-1); | |
| 289 }, | |
| 290 | |
| 291 'Right': () { | |
| 292 cursor.clearSelection(); | |
| 293 cursor._pos = cursor._pos.moveColumn(+1); | |
| 294 }, | |
| 295 'Shift-Right': () { | |
| 296 if (cursor._toPos == null) { | |
| 297 cursor._toPos = cursor._pos; | |
| 298 } | |
| 299 cursor._pos = cursor._pos.moveColumn(+1); | |
| 300 }, | |
| 301 'Up': () { | |
| 302 cursor.clearSelection(); | |
| 303 // TODO(jimhug): up and down lose column info on shorter lines. | |
| 304 cursor._pos = cursor._pos.moveLine(-1); | |
| 305 }, | |
| 306 'Shift-Up': () { | |
| 307 if (cursor._toPos == null) { | |
| 308 cursor._toPos = cursor._pos; | |
| 309 } | |
| 310 cursor._pos = cursor._pos.moveLine(-1); | |
| 311 }, | |
| 312 'Down': () { | |
| 313 cursor.clearSelection(); | |
| 314 cursor._pos = cursor._pos.moveLine(+1); | |
| 315 }, | |
| 316 'Shift-Down': () { | |
| 317 if (cursor._toPos == null) { | |
| 318 cursor._toPos = cursor._pos; | |
| 319 } | |
| 320 cursor._pos = cursor._pos.moveLine(+1); | |
| 321 }, | |
| 322 | |
| 323 'Meta-Up': () { | |
| 324 cursor.clearSelection(); | |
| 325 cursor._pos = _editor._code.start; | |
| 326 }, | |
| 327 'Meta-Shift-Up': () { | |
| 328 if (cursor._toPos == null) { | |
| 329 cursor._toPos = cursor._pos; | |
| 330 } | |
| 331 cursor._pos = _editor._code.start; | |
| 332 }, | |
| 333 | |
| 334 'Meta-Down': () { | |
| 335 cursor.clearSelection(); | |
| 336 cursor._pos = _editor._code.end; | |
| 337 }, | |
| 338 'Meta-Shift-Down': () { | |
| 339 if (cursor._toPos == null) { | |
| 340 cursor._toPos = cursor._pos; | |
| 341 } | |
| 342 cursor._pos = _editor._code.end; | |
| 343 }, | |
| 344 | |
| 345 'Delete': () { | |
| 346 //TODO(jimhug): go back to beginning of line when appropriate. | |
| 347 if (cursor._toPos == null) { | |
| 348 cursor._toPos = cursor._pos.moveColumn(-1); | |
| 349 } | |
| 350 cursor.deleteSelection(); | |
| 351 }, | |
| 352 'Control-D': () { | |
| 353 if (cursor._toPos == null) { | |
| 354 cursor._toPos = cursor._pos.moveColumn(+1); | |
| 355 } | |
| 356 cursor.deleteSelection(); | |
| 357 }, | |
| 358 | |
| 359 'Meta-A': () { | |
| 360 cursor._pos = _editor._code.start; | |
| 361 cursor._toPos = _editor._code.end; | |
| 362 }, | |
| 363 | |
| 364 '.' : () { | |
| 365 // TODO(jimhug): complete hints here | |
| 366 cursor.write('.'); | |
| 367 }, | |
| 368 | |
| 369 '}' : () { | |
| 370 // TODO(jimhug): Get indentation right. | |
| 371 cursor.write('}'); | |
| 372 }, | |
| 373 | |
| 374 | |
| 375 'Enter': () { | |
| 376 cursor.write('\n'); | |
| 377 // TODO(jimhug): Indent on the new line appropriately... | |
| 378 }, | |
| 379 | |
| 380 'Tab': () { | |
| 381 // TODO(jimhug): force tab to always "properly" indent the line | |
| 382 cursor.write(' '); | |
| 383 }, | |
| 384 | |
| 385 'Space': () => cursor.write(' '), | |
| 386 // This seems to be a common typo, so just allow it. | |
| 387 'Shift-Space': () => cursor.write(' '), | |
| 388 | |
| 389 'Meta-G': () => cursor.write(CODE), | |
| 390 'Meta-H': () => cursor.write(HCODE), | |
| 391 | |
| 392 'Meta-P': () => cursor._pos.block.parse(), | |
| 393 | |
| 394 'Shift-Enter': run, | |
| 395 'Meta-Enter': run, | |
| 396 }; | |
| 397 | |
| 398 _bindings = new KeyBindings(_textInputArea, bindings, | |
| 399 (String text) { cursor.write(text); }, | |
| 400 (String key) { | |
| 401 // Use native bindings for cut and paste | |
| 402 if (key == 'Meta-V') return false; | |
| 403 if (key == 'Control-V') return false; | |
| 404 | |
| 405 // TODO(jimhug): No good, very bad hack. | |
| 406 if (key == 'Meta-X' || key == 'Control-X') { | |
| 407 window.setTimeout(() { | |
| 408 cursor.deleteSelection(); | |
| 409 _editor._redraw(); | |
| 410 }, 0); | |
| 411 return false; | |
| 412 } | |
| 413 | |
| 414 if (key == 'Meta-C') return false; | |
| 415 if (key == 'Control-C') return false; | |
| 416 | |
| 417 // Use native bindings for dev tools | |
| 418 if (key == 'Alt-Meta-I') return false; | |
| 419 if (key == 'Control-Shift-I') return false; | |
| 420 window.console.log('Unbound key "$key"'); | |
| 421 return true; | |
| 422 }); | |
| 423 } | |
| 424 | |
| 425 void handleMessage(String prefix, String message, SourceSpan span) { | |
| 426 var m = new Message(prefix, message, span); | |
| 427 _errors.addMessage(m); | |
| 428 } | |
| 429 | |
| 430 void showSpan(SourceSpan span) { | |
| 431 // TODO(jimhug): Get code from correct file. | |
| 432 var code = _editor._code; | |
| 433 var p1 = new CodePosition(code, span.start); | |
| 434 var p2 = new CodePosition(code, span.end); | |
| 435 _editor._cursor._pos = p1.toLeaf(); | |
| 436 _editor._cursor._toPos = p2.toLeaf(); | |
| 437 _editor._redraw(); | |
| 438 } | |
| 439 | |
| 440 void run() { | |
| 441 _output.contentDocument.body.innerHTML = | |
| 442 '<h3 style="color:green">Compiling...</h3>'; | |
| 443 _errors.clear(); | |
| 444 | |
| 445 window.setTimeout( () { | |
| 446 final sw = new Stopwatch(); | |
| 447 sw.start(); | |
| 448 var code = _editor.getCode(); | |
| 449 world.files.writeString(DART_FILENAME, code); | |
| 450 options.enableAsserts = true; | |
| 451 options.enableTypeChecks = true; | |
| 452 world.reset(); | |
| 453 var success = world.compile(); | |
| 454 sw.stop(); | |
| 455 | |
| 456 if (success) { | |
| 457 _output.contentDocument.body.innerHTML = ''; | |
| 458 inject(world.getGeneratedCode(), _output.contentWindow); | |
| 459 } else { | |
| 460 _output.contentDocument.body.innerHTML = | |
| 461 '<h3 style="color:red">Compilation failed</h3>'; | |
| 462 } | |
| 463 | |
| 464 print('compiled in ${sw.elapsedInMs()}msec'); | |
| 465 }, 0); | |
| 466 } | |
| 467 | |
| 468 void focusKeys(Editor editor) { | |
| 469 _editor = editor; | |
| 470 cursor = editor._cursor; | |
| 471 _textInputArea.focus(); | |
| 472 } | |
| 473 } | |
| 474 | |
| 475 | |
| 476 class Editor { | |
| 477 Shell _shell; | |
| 478 Cursor _cursor; | |
| 479 CodeBlock _code; | |
| 480 var _node; | |
| 481 | |
| 482 // Some temp state for mouse down and selections | |
| 483 bool _isSelecting; | |
| 484 int _lastClickTime; | |
| 485 bool _didDoubleClick; | |
| 486 | |
| 487 Editor(this._shell) { | |
| 488 _node = document.createElement('div'); | |
| 489 _node.className = 'editor'; | |
| 490 | |
| 491 _code = new BlockBlock(null, 0); | |
| 492 _code.text = CODE; | |
| 493 _code.top = 0; | |
| 494 _node.appendChild(_code._node); | |
| 495 | |
| 496 _cursor = new Cursor(_code.start); | |
| 497 _node.appendChild(_cursor._node); | |
| 498 | |
| 499 _node.addEventListener('mousedown', mousedown, false); | |
| 500 _node.addEventListener('mousemove', mousemove, false); | |
| 501 _node.addEventListener('mouseup', mouseup, false); | |
| 502 | |
| 503 // TODO(jimhug): Final bit to make region selection clean. | |
| 504 //this.node.addEventListener('mouseout', this, false); | |
| 505 | |
| 506 // TODO(jimhug): Lazy rendering should be triggered by this. | |
| 507 //_node.addEventListener('scroll', this, false); | |
| 508 } | |
| 509 | |
| 510 String getCode() { | |
| 511 return _code.text; | |
| 512 } | |
| 513 | |
| 514 void goto(int line, int column) { | |
| 515 _cursor._pos = _code.getPosition(line, column); | |
| 516 } | |
| 517 | |
| 518 void mousedown(MouseEvent e) { | |
| 519 // for shift click, create selection region | |
| 520 if (e.shiftKey) { | |
| 521 _cursor._toPos = _cursor._pos; | |
| 522 } else { | |
| 523 _cursor.clearSelection(); | |
| 524 } | |
| 525 _cursor._pos = _code.positionFromMouse(e); | |
| 526 focus(); | |
| 527 e.preventDefault(); | |
| 528 | |
| 529 _isSelecting = true; | |
| 530 } | |
| 531 | |
| 532 void mousemove(MouseEvent e) { | |
| 533 // TODO(jimhug): Would REALLY like to check that button is down here! | |
| 534 if (_isSelecting) { | |
| 535 if (_cursor._toPos == null) { | |
| 536 _cursor._toPos = _cursor._pos; | |
| 537 } | |
| 538 _cursor._pos = _code.positionFromMouse(e); | |
| 539 e.preventDefault(); | |
| 540 _redraw(); | |
| 541 } | |
| 542 } | |
| 543 | |
| 544 void mouseup(MouseEvent e) { | |
| 545 _isSelecting = false; | |
| 546 if (_cursor.emptySelection) { | |
| 547 _cursor.clearSelection(); | |
| 548 } | |
| 549 } | |
| 550 | |
| 551 void scrollToVisible(CodePosition pos) { | |
| 552 var top = _node.scrollTop; | |
| 553 var height = _node.getBoundingClientRect().height; | |
| 554 var pt = pos.getPoint(); | |
| 555 | |
| 556 if (pt.y < top) { | |
| 557 _node.scrollTop = pt.y; | |
| 558 } else if (pt.y > top + height - LINE_HEIGHT) { | |
| 559 var H = LINE_HEIGHT * ((height ~/ LINE_HEIGHT) - 1); | |
| 560 _node.scrollTop = Math.max(pt.y - H, 0); | |
| 561 } | |
| 562 } | |
| 563 | |
| 564 void _redraw() { | |
| 565 scrollToVisible(_cursor._pos); | |
| 566 _code._redraw(); | |
| 567 _cursor._redraw(); | |
| 568 } | |
| 569 | |
| 570 void focus() { | |
| 571 _shell.focusKeys(this); | |
| 572 _cursor._visible = true; | |
| 573 _redraw(); | |
| 574 } | |
| 575 | |
| 576 void blur() { | |
| 577 _shell.blurKeys(this); | |
| 578 _cursor._visible = false; | |
| 579 _redraw(); | |
| 580 } | |
| 581 } | |
| 582 | |
| 583 class Point { | |
| 584 final int x, y; | |
| 585 const Point(this.x, this.y); | |
| 586 } | |
| 587 | |
| 588 class LineColumn { | |
| 589 final int line, column; | |
| 590 LineColumn(this.line, this.column); | |
| 591 } | |
| 592 | |
| 593 class Cursor { | |
| 594 CodePosition _pos; | |
| 595 | |
| 596 CodePosition _toPos; | |
| 597 bool _visible = true; | |
| 598 var _node; | |
| 599 | |
| 600 var _cursorNode; | |
| 601 var _selectionNode; | |
| 602 | |
| 603 Cursor(this._pos) { | |
| 604 _node = document.createElement('div'); | |
| 605 _node.className = 'cursorDiv'; | |
| 606 } | |
| 607 | |
| 608 bool get emptySelection() { | |
| 609 return _toPos == null || | |
| 610 (_toPos.block == _pos.block && _toPos.offset == _pos.offset); | |
| 611 } | |
| 612 | |
| 613 _redraw() { | |
| 614 // Approach is to kill and recreate everything on a redraw. | |
| 615 // There are lots of potential improvements if this proves costly. | |
| 616 // However: If we don't do this we need a different dance to make cursor | |
| 617 // blinking disabled when it is moving. | |
| 618 | |
| 619 _node.innerHTML = ''; | |
| 620 if (!_visible) return; | |
| 621 | |
| 622 _cursorNode = document.createElement('div'); | |
| 623 _cursorNode.className = 'cursor blink'; | |
| 624 _cursorNode.style.setProperty('height', '${LINE_HEIGHT}px'); | |
| 625 | |
| 626 var p = _pos.getPoint(); | |
| 627 _cursorNode.style.setProperty('left', '${p.x}px'); | |
| 628 _cursorNode.style.setProperty('top', '${p.y}px'); | |
| 629 _node.appendChild(_cursorNode); | |
| 630 | |
| 631 if (_toPos == null) return; | |
| 632 | |
| 633 void addDiv(top, left, height, width) { | |
| 634 var child = document.createElement('div'); | |
| 635 child.className = 'selection'; | |
| 636 child.style.setProperty('left', '${left}px'); | |
| 637 child.style.setProperty('top', '${top}px'); | |
| 638 | |
| 639 child.style.setProperty('height', '${height}px'); | |
| 640 if (width == null) { | |
| 641 child.style.setProperty('right', '0px'); | |
| 642 } else { | |
| 643 child.style.setProperty('width', '${width}px'); | |
| 644 } | |
| 645 _node.appendChild(child); | |
| 646 } | |
| 647 | |
| 648 var toP = _toPos.getPoint(); | |
| 649 // Same line - only one line to highlight | |
| 650 if (toP.y == p.y) { | |
| 651 if (toP.x < p.x) { | |
| 652 addDiv(p.y, toP.x, LINE_HEIGHT, p.x - toP.x); | |
| 653 } else { | |
| 654 addDiv(p.y, p.x, LINE_HEIGHT, toP.x - p.x); | |
| 655 } | |
| 656 } else { | |
| 657 if (toP.y < p.y) { | |
| 658 var tmp = toP; toP = p; p = tmp; | |
| 659 } | |
| 660 addDiv(p.y, p.x, LINE_HEIGHT, null); | |
| 661 if (toP.y > p.y + LINE_HEIGHT) { | |
| 662 addDiv(p.y + LINE_HEIGHT, 0, toP.y - p.y - LINE_HEIGHT, null); | |
| 663 } | |
| 664 addDiv(toP.y, 0, LINE_HEIGHT, toP.x); | |
| 665 } | |
| 666 | |
| 667 // TODO(jimhug): separate out - this makes default copy/cut work | |
| 668 var p0 = _pos.toRoot(); | |
| 669 var p1 = _toPos.toRoot(); | |
| 670 | |
| 671 var i0 = p0.offset; | |
| 672 var i1 = p1.offset; | |
| 673 if (i1 < i0) { | |
| 674 var tmp = i1; i1 = i0; i0 = tmp; | |
| 675 } | |
| 676 var text = p0.block.text.substring(i0, i1); | |
| 677 shell._textInputArea.value = text; | |
| 678 shell._textInputArea.select(); | |
| 679 } | |
| 680 | |
| 681 void clearSelection() { | |
| 682 _toPos = null; | |
| 683 } | |
| 684 | |
| 685 void moveColumn(int delta) { | |
| 686 _pos = _pos.moveColumn(delta); | |
| 687 } | |
| 688 | |
| 689 void moveLine(int delta) { | |
| 690 _pos = _pos.moveLine(delta); | |
| 691 } | |
| 692 | |
| 693 void deleteSelection() { | |
| 694 if (_toPos == null) return; | |
| 695 | |
| 696 var p0 = _pos; | |
| 697 var p1 = _toPos; | |
| 698 | |
| 699 if (p0.block != p1.block) { | |
| 700 // move up to root and resolve there... | |
| 701 p0 = p0.toRoot(); | |
| 702 p1 = p1.toRoot(); | |
| 703 } | |
| 704 assert(p0.block == p1.block); | |
| 705 | |
| 706 if (p1.offset < p0.offset) { | |
| 707 var tmp = p0; p0 = p1; p1 = tmp; | |
| 708 } | |
| 709 p0.block.delete(p0.offset, p1.offset); | |
| 710 _pos = p0.toLeaf(); | |
| 711 _toPos = null; | |
| 712 } | |
| 713 | |
| 714 void write(String text) { | |
| 715 // TODO(jimhug): combine insert and delete to optimize | |
| 716 deleteSelection(); | |
| 717 _pos.block.insertText(_pos.offset, text); | |
| 718 _pos.block._redraw(); // TODO(jimhug): Egregious hack. | |
| 719 _pos = _pos.moveColumn(text.length); | |
| 720 } | |
| 721 } | |
| 722 | |
| 723 class CodePosition { | |
| 724 final CodeBlock block; | |
| 725 final int offset; | |
| 726 | |
| 727 CodePosition(this.block, this.offset); | |
| 728 | |
| 729 CodePosition moveLine(int delta) { | |
| 730 if (delta == 0) return this; | |
| 731 | |
| 732 var lineCol = getLineColumn(); | |
| 733 return block.getPosition(lineCol.line + delta, lineCol.column); | |
| 734 } | |
| 735 | |
| 736 CodePosition moveColumn(int delta) { | |
| 737 return block.moveToOffset(offset + delta); | |
| 738 } | |
| 739 | |
| 740 Point getPoint() { | |
| 741 return block.offsetToPoint(offset); | |
| 742 } | |
| 743 | |
| 744 LineColumn getLineColumn() { | |
| 745 return block.getLineColumn(offset); | |
| 746 } | |
| 747 | |
| 748 CodePosition toRoot() { | |
| 749 if (block._parent === null) return this; | |
| 750 | |
| 751 var ret = new CodePosition(block._parent, | |
| 752 block._parent.getOffset(block) + offset); | |
| 753 return ret.toRoot(); | |
| 754 } | |
| 755 | |
| 756 CodePosition toLeaf() { | |
| 757 return block.moveToOffset(offset); | |
| 758 } | |
| 759 } | |
| 760 | |
| 761 | |
| 762 /** | |
| 763 * Every [CodeBlock] must provide the following: | |
| 764 * - a node to render it - purely as vertical div | |
| 765 * - a height in real pixels and in code lines | |
| 766 * - proper interaction with CodePosition | |
| 767 * - accurate and working get/set for text property | |
| 768 * - appropriate integration into parsing | |
| 769 * - support to hold onto annotations of various sorts | |
| 770 */ | |
| 771 class BlockChildren implements Iterable<CodeBlock> { | |
| 772 BlockBlock _parent; | |
| 773 BlockChildren(this._parent); | |
| 774 | |
| 775 Iterator<CodeBlock> iterator() { | |
| 776 return new BlockChildrenIterator(_parent._firstChild); | |
| 777 } | |
| 778 } | |
| 779 | |
| 780 class BlockChildrenIterator implements Iterator<CodeBlock> { | |
| 781 CodeBlock _child; | |
| 782 BlockChildrenIterator(this._child); | |
| 783 | |
| 784 // TODO(jimhug): current + moveNext() is much more sane. | |
| 785 CodeBlock next() { | |
| 786 var ret = _child; | |
| 787 _child = _child._nextSibling; | |
| 788 return ret; | |
| 789 } | |
| 790 | |
| 791 bool hasNext() => _child !== null; | |
| 792 } | |
| 793 | |
| 794 | |
| 795 /** | |
| 796 * Block block... first and last children are special except for top file. | |
| 797 */ | |
| 798 class BlockBlock extends CodeBlock { | |
| 799 CodeBlock _firstChild; | |
| 800 CodeBlock _lastChild; | |
| 801 | |
| 802 Iterable<CodeBlock> children; | |
| 803 | |
| 804 BlockBlock(CodeBlock parent, int depth): super(parent, depth) { | |
| 805 text = ''; | |
| 806 children = new BlockChildren(this); | |
| 807 } | |
| 808 | |
| 809 CodePosition get start() => _firstChild.start; | |
| 810 | |
| 811 CodePosition get end() => _lastChild.end; | |
| 812 | |
| 813 int get size() { | |
| 814 var ret = 0; | |
| 815 for (var child in children) ret += child.size; | |
| 816 return ret; | |
| 817 } | |
| 818 | |
| 819 int get lineCount() { | |
| 820 var ret = 0; | |
| 821 for (var child in children) ret += child.lineCount; | |
| 822 return ret; | |
| 823 } | |
| 824 | |
| 825 // TODO(jimhug): This property is expensive here - should it be a prop? | |
| 826 String get text() { | |
| 827 var ret = new StringBuffer(); | |
| 828 for (var child in children) ret.add(child.text); | |
| 829 return ret.toString(); | |
| 830 } | |
| 831 | |
| 832 int getOffset(CodeBlock forChild) { | |
| 833 var ret = 0; | |
| 834 for (var child in children) { | |
| 835 if (child == forChild) return ret; | |
| 836 ret += child.size; | |
| 837 } | |
| 838 throw "child missing"; | |
| 839 } | |
| 840 | |
| 841 int getLine(CodeBlock forChild) { | |
| 842 var ret = 0; | |
| 843 for (var child in children) { | |
| 844 if (child == forChild) return ret; | |
| 845 ret += child.lineCount; | |
| 846 } | |
| 847 throw "child missing"; | |
| 848 } | |
| 849 | |
| 850 _addChildAfter(CodeBlock addAfterChild, CodeBlock child) { | |
| 851 markDirty(); | |
| 852 child._nextSibling = addAfterChild._nextSibling; | |
| 853 child._previousSibling = addAfterChild; | |
| 854 addAfterChild._nextSibling = child; | |
| 855 | |
| 856 // Add child's node into our DOM tree. | |
| 857 if (child._nextSibling === null) { | |
| 858 _node.appendChild(child._node); | |
| 859 _lastChild = child; | |
| 860 } else { | |
| 861 _node.insertBefore(child._node, child._nextSibling._node); | |
| 862 } | |
| 863 } | |
| 864 | |
| 865 _removeChild(CodeBlock child) { | |
| 866 markDirty(); | |
| 867 if (child._previousSibling !== null) { | |
| 868 child._previousSibling._nextSibling = child._nextSibling; | |
| 869 } else { | |
| 870 _firstChild = child._nextSibling; | |
| 871 } | |
| 872 if (child._nextSibling !== null) { | |
| 873 child._nextSibling._previousSibling = child._previousSibling; | |
| 874 } else { | |
| 875 _lastChild = child._previousSibling; | |
| 876 } | |
| 877 | |
| 878 // Remove child's node from our DOM tree. | |
| 879 _node.removeChild(child._node); | |
| 880 } | |
| 881 | |
| 882 void set text(String newText) { | |
| 883 final sw = new Stopwatch(); | |
| 884 sw.start(); | |
| 885 | |
| 886 _firstChild = _lastChild = null; | |
| 887 | |
| 888 final src = new SourceFile('fake.dart', newText); | |
| 889 int start = 0; | |
| 890 while (start != -1) { | |
| 891 var child = new TextBlock(this, null, _depth + 1); | |
| 892 if (_lastChild === null) { | |
| 893 _firstChild = _lastChild = child; | |
| 894 _node.appendChild(child._node); | |
| 895 } else { | |
| 896 _addChildAfter(_lastChild, child); | |
| 897 } | |
| 898 start = child.tokenizeInto(src, start); | |
| 899 } | |
| 900 | |
| 901 sw.stop(); | |
| 902 | |
| 903 print('create structure in ${sw.elapsedInMs()}msec'); | |
| 904 } | |
| 905 | |
| 906 void insertText(int offset, String newText) { | |
| 907 var index = 0; | |
| 908 for (var child in children) { | |
| 909 var childSize = child.size; | |
| 910 if (offset < childSize) { | |
| 911 child.insertText(offset, newText); | |
| 912 return; | |
| 913 } else if (offset == childSize) { | |
| 914 // TODO(jimhug): Nasty merging of text and block structure here. | |
| 915 var newChild = new TextBlock(this, newText, _depth + 1); | |
| 916 _addChildAfter(child, newChild); | |
| 917 return; | |
| 918 } | |
| 919 offset -= childSize; | |
| 920 index++; | |
| 921 } | |
| 922 // TODO: nesting at this level | |
| 923 throw "help"; | |
| 924 } | |
| 925 | |
| 926 void delete(int from, int to) { | |
| 927 assert(from <= to); | |
| 928 var keepChild = null; | |
| 929 for (var child = _firstChild; child !== null; child = child._nextSibling) { | |
| 930 var childSize = child.size; | |
| 931 if (keepChild !== null) { | |
| 932 _removeChild(child); | |
| 933 if (to <= childSize) { | |
| 934 // abstraction violation!!! | |
| 935 keepChild._text += child._text.substring(to); | |
| 936 return; | |
| 937 } | |
| 938 } else if (from <= childSize) { | |
| 939 if (to < childSize) { | |
| 940 child.delete(from, to); | |
| 941 return; | |
| 942 } else { | |
| 943 child.delete(from, childSize); | |
| 944 keepChild = child; | |
| 945 } | |
| 946 } | |
| 947 from -= childSize; | |
| 948 to -= childSize; | |
| 949 } | |
| 950 // TODO: nesting at this level | |
| 951 throw "help"; | |
| 952 } | |
| 953 | |
| 954 _redraw() { | |
| 955 if (!_dirty) return; | |
| 956 _dirty = false; | |
| 957 | |
| 958 var childTop = 0; | |
| 959 for (var child = _firstChild; child !== null; child = child._nextSibling) { | |
| 960 // Note: Performance here relies on lazy setter in CodeBlock. | |
| 961 child.top = childTop; | |
| 962 child._redraw(); | |
| 963 childTop += child.height; | |
| 964 } | |
| 965 | |
| 966 height = childTop; | |
| 967 } | |
| 968 | |
| 969 CodePosition moveToOffset(int offset) { | |
| 970 for (var child in children) { | |
| 971 var childSize = child.size; | |
| 972 if (offset < childSize) { | |
| 973 return child.moveToOffset(offset); | |
| 974 } | |
| 975 offset -= childSize; | |
| 976 } | |
| 977 // TODO: nesting at this level | |
| 978 return end; | |
| 979 } | |
| 980 | |
| 981 CodePosition positionFromPoint(int x, int y) { | |
| 982 if (y < top) return start; | |
| 983 | |
| 984 for (var child in children) { | |
| 985 if (child.top <= y && (child.top + child.height) >= y) { | |
| 986 return child.positionFromPoint(x, y - child.top); | |
| 987 } | |
| 988 } | |
| 989 // TODO: next level of nesting... | |
| 990 return end; | |
| 991 } | |
| 992 | |
| 993 CodePosition getPosition(int line, int column) { | |
| 994 if (line < 0) return start; | |
| 995 for (var child in children) { | |
| 996 if (line < child.lineCount) return child.getPosition(line, column); | |
| 997 | |
| 998 line -= child.lineCount; | |
| 999 } | |
| 1000 return end; // TODO | |
| 1001 } | |
| 1002 | |
| 1003 // These are local line/column to this block?????? | |
| 1004 LineColumn getLineColumn(int offset) { | |
| 1005 if (offset < 0) return new LineColumn(0, 0); | |
| 1006 | |
| 1007 int childLine = 0; | |
| 1008 for (var child in children) { | |
| 1009 var childSize = child.size; | |
| 1010 if (offset < childSize) { | |
| 1011 // TODO: This needs modification!!! | |
| 1012 var ret = child.getLineColumn(offset); | |
| 1013 return new LineColumn(ret.line + childLine, ret.column); | |
| 1014 } | |
| 1015 offset -= childSize; | |
| 1016 childLine += child.lineCount; | |
| 1017 } | |
| 1018 // TODO: nesting at this level | |
| 1019 return new LineColumn(lineCount, 0); // ??? wrong end column | |
| 1020 } | |
| 1021 | |
| 1022 Point offsetToPoint(int offset) { | |
| 1023 if (offset < 0) return new Point(0, 0); | |
| 1024 | |
| 1025 for (var child in children) { | |
| 1026 var childSize = child.size; | |
| 1027 if (offset < childSize) { | |
| 1028 var ret = child.offsetToPoint(offset); | |
| 1029 return new Point(ret.x, ret.y + child.top); | |
| 1030 } | |
| 1031 offset -= childSize; | |
| 1032 } | |
| 1033 // TODO: nesting at this level | |
| 1034 return new Point(0, top + height); | |
| 1035 } | |
| 1036 } | |
| 1037 | |
| 1038 /** | |
| 1039 * Pure text block - terminals. | |
| 1040 */ | |
| 1041 class TextBlock extends CodeBlock { | |
| 1042 List<int> _lineStarts; | |
| 1043 String _text; | |
| 1044 | |
| 1045 TextBlock(CodeBlock parent, this._text, int depth): super(parent, depth) { | |
| 1046 //window.console.warn('tb: "$_text"'); | |
| 1047 } | |
| 1048 | |
| 1049 int get size() => _text.length; | |
| 1050 | |
| 1051 int get lineCount() => _lineStarts.length; | |
| 1052 | |
| 1053 String get text() => _text; | |
| 1054 | |
| 1055 void set text(String newText) { | |
| 1056 _text = newText; | |
| 1057 markDirty(); | |
| 1058 } | |
| 1059 | |
| 1060 void insertText(int offset, String newText) { | |
| 1061 _text = _text.substring(0, offset) + newText + _text.substring(offset); | |
| 1062 markDirty(); | |
| 1063 } | |
| 1064 | |
| 1065 void delete(int from, int to) { | |
| 1066 assert(from <= to); | |
| 1067 assert(to <= _text.length); | |
| 1068 markDirty(); | |
| 1069 | |
| 1070 if (to == _text.length) { | |
| 1071 if (from == 0) { | |
| 1072 _parent._removeChild(this); | |
| 1073 } else { | |
| 1074 _text = _text.substring(0, from); | |
| 1075 } | |
| 1076 } else { | |
| 1077 _text = _text.substring(0, from) + _text.substring(to); | |
| 1078 } | |
| 1079 } | |
| 1080 | |
| 1081 int tokenizeInto(SourceFile src, int start) { | |
| 1082 _lineStarts = new List<int>(); | |
| 1083 _lineStarts.add(start); | |
| 1084 | |
| 1085 // classify my text and create siblings and parents as needed | |
| 1086 var html = new StringBuffer(); | |
| 1087 Tokenizer tokenizer = new Tokenizer(src, /*skipWhitespace:*/false, start); | |
| 1088 | |
| 1089 int depth = 0; | |
| 1090 | |
| 1091 // TODO(jimhug): REALLY INEFFICIENT! | |
| 1092 void addLineStarts(Token token) { | |
| 1093 // TODO(jimhug): Should we just make the Tokenizer do this directly? | |
| 1094 final text = src.text; | |
| 1095 for (int index = token.start; index < token.end; index++) { | |
| 1096 if (text.charCodeAt(index) == 10/*'\n'*/) { | |
| 1097 _lineStarts.add(index - start + 1); | |
| 1098 } | |
| 1099 } | |
| 1100 } | |
| 1101 | |
| 1102 // TODO(jimhug): Add whitespace blocks? | |
| 1103 while (true) { | |
| 1104 var token = tokenizer.next(); | |
| 1105 | |
| 1106 if (token.kind == TokenKind.END_OF_FILE) { | |
| 1107 _node.innerHTML = html.toString(); | |
| 1108 if (start == 0) _text = src.text; | |
| 1109 else _text = src.text.substring(start); | |
| 1110 height = lineCount * LINE_HEIGHT; | |
| 1111 // update parent | |
| 1112 return -1; | |
| 1113 } else if (token.kind == TokenKind.WHITESPACE) { | |
| 1114 // TODO(jimhug): Special handling for pure whitespace divs? | |
| 1115 if (src.text.charCodeAt(token.end-1) == 10/*'\n'*/) { | |
| 1116 // Model 1 - create siblings at any "true" line break | |
| 1117 // -- set my from source | |
| 1118 _text = src.text.substring(start, token.end); | |
| 1119 _node.innerHTML = html.toString(); | |
| 1120 height = lineCount * LINE_HEIGHT; | |
| 1121 // update parent... | |
| 1122 return token.end; | |
| 1123 } | |
| 1124 } else if (token.kind == TokenKind.COMMENT) { | |
| 1125 // TODO(jimhug): These may be the most fun blocks to handle... | |
| 1126 addLineStarts(token); | |
| 1127 } else if (token.kind == TokenKind.STRING) { | |
| 1128 addLineStarts(token); | |
| 1129 } else if (token.kind == TokenKind.STRING_PART) { | |
| 1130 addLineStarts(token); | |
| 1131 } | |
| 1132 | |
| 1133 final kind = classify(token); | |
| 1134 final stringClass = ''; | |
| 1135 final text = htmlEscape(token.text); | |
| 1136 if (kind != null) { | |
| 1137 html.add('<span class="$kind $stringClass">$text</span>'); | |
| 1138 } else { | |
| 1139 html.add('<span>$text</span>'); | |
| 1140 } | |
| 1141 } | |
| 1142 } | |
| 1143 | |
| 1144 _redraw() { | |
| 1145 if (!_dirty) return; | |
| 1146 _dirty = false; | |
| 1147 | |
| 1148 var initialText = _text; | |
| 1149 var end = tokenizeInto(new SourceFile('fake.dart', _text), 0); | |
| 1150 if (_text.length < initialText.length) { | |
| 1151 // ??? How to know we want a new block ??? | |
| 1152 var extraText = initialText.substring(_text.length); | |
| 1153 _parent.insertText(_parent.getOffset(this) + _text.length, extraText); | |
| 1154 } | |
| 1155 } | |
| 1156 | |
| 1157 CodePosition moveToOffset(int offset) { | |
| 1158 if (offset < 0 || offset >= _text.length) { | |
| 1159 return _parent.moveToOffset(_parent.getOffset(this) + offset); | |
| 1160 } | |
| 1161 return new CodePosition(this, offset); | |
| 1162 } | |
| 1163 | |
| 1164 CodePosition positionFromPoint(int x, int y) { | |
| 1165 return getPosition((y / LINE_HEIGHT).floor(), (x / CHAR_WIDTH).round()); | |
| 1166 } | |
| 1167 | |
| 1168 CodePosition getPosition(int line, int column) { | |
| 1169 if (line < 0 || line >= lineCount) { | |
| 1170 return _parent.getPosition(_parent.getLine(this) + line, column); | |
| 1171 } | |
| 1172 | |
| 1173 int maxOffset; | |
| 1174 if (line < _lineStarts.length - 1) { | |
| 1175 maxOffset = _lineStarts[line + 1] - 1; | |
| 1176 } else { | |
| 1177 maxOffset = _text.length - 1; | |
| 1178 } | |
| 1179 | |
| 1180 final offset = Math.min(_lineStarts[line] + column, maxOffset); | |
| 1181 | |
| 1182 return new CodePosition(this, offset); | |
| 1183 } | |
| 1184 | |
| 1185 // These are local line/column to this block | |
| 1186 LineColumn getLineColumn(int offset) { | |
| 1187 if (_lineStarts === null) { | |
| 1188 return new LineColumn(0, 0); | |
| 1189 } | |
| 1190 // TODO(jimhug): Binary search would be faster but more complicated. | |
| 1191 int previousStart = 0; | |
| 1192 int line = 1; | |
| 1193 for (; line < _lineStarts.length; line++) { | |
| 1194 int start = _lineStarts[line]; | |
| 1195 if (start > offset) { | |
| 1196 break; | |
| 1197 } | |
| 1198 previousStart = start; | |
| 1199 } | |
| 1200 return new LineColumn(line - 1, offset - previousStart); | |
| 1201 } | |
| 1202 | |
| 1203 Point offsetToPoint(int offset) { | |
| 1204 LineColumn lc = getLineColumn(offset); | |
| 1205 return new Point(lc.column * CHAR_WIDTH, top + (lc.line * LINE_HEIGHT)); | |
| 1206 } | |
| 1207 } | |
| 1208 | |
| 1209 | |
| 1210 class CodeBlock { | |
| 1211 CodeBlock _parent; | |
| 1212 | |
| 1213 CodeBlock _previousSibling; | |
| 1214 CodeBlock _nextSibling; | |
| 1215 | |
| 1216 int _depth = 0; | |
| 1217 | |
| 1218 bool _dirty = true; | |
| 1219 var _node; | |
| 1220 int _top, _height; | |
| 1221 | |
| 1222 CodeBlock(this._parent, this._depth) { | |
| 1223 _node = document.createElement('div'); | |
| 1224 _node.className = 'code'; // TODO - different kinds of nodes | |
| 1225 } | |
| 1226 | |
| 1227 abstract int size(); | |
| 1228 abstract int get lineCount(); | |
| 1229 | |
| 1230 abstract String get text(); | |
| 1231 | |
| 1232 abstract void set text(String newText); | |
| 1233 | |
| 1234 abstract CodePosition moveToOffset(int offset); | |
| 1235 | |
| 1236 CodePosition get start() => new CodePosition(this, 0); | |
| 1237 | |
| 1238 CodePosition get end() => new CodePosition(this, size); | |
| 1239 | |
| 1240 int getOffset(CodeBlock forChild) { | |
| 1241 throw "child missing"; | |
| 1242 } | |
| 1243 | |
| 1244 int getLine(CodeBlock forChild) { | |
| 1245 throw "child missing"; | |
| 1246 } | |
| 1247 | |
| 1248 _removeChild(CodeBlock child) { | |
| 1249 throw "child missing"; | |
| 1250 } | |
| 1251 | |
| 1252 void parse() { | |
| 1253 final source = new SourceFile('fake.dart', text); | |
| 1254 var p = new Parser(source); | |
| 1255 var cu = p.compilationUnit(); | |
| 1256 } | |
| 1257 | |
| 1258 void markDirty() { | |
| 1259 if (!_dirty) { | |
| 1260 _dirty = true; | |
| 1261 if (_parent != null) { | |
| 1262 _parent._dirty = true; | |
| 1263 } | |
| 1264 } | |
| 1265 } | |
| 1266 | |
| 1267 int get top() => _top; | |
| 1268 | |
| 1269 void set top(int newTop) { | |
| 1270 if (newTop != _top) { | |
| 1271 _top = newTop; | |
| 1272 _node.style.setProperty('top', '${_top}px'); | |
| 1273 } | |
| 1274 } | |
| 1275 | |
| 1276 int get height() => _height; | |
| 1277 | |
| 1278 void set height(int newHeight) { | |
| 1279 if (newHeight != _height) { | |
| 1280 _height = newHeight; | |
| 1281 _node.style.setProperty('height', '${_height}px'); | |
| 1282 } | |
| 1283 } | |
| 1284 | |
| 1285 abstract void insertText(int offset, String newText); | |
| 1286 | |
| 1287 abstract void delete(int from, int to); | |
| 1288 | |
| 1289 CodePosition positionFromMouse(MouseEvent p) { | |
| 1290 var box = _node.getBoundingClientRect(); | |
| 1291 int y = p.clientY - box.top; | |
| 1292 int x = p.clientX - box.left; | |
| 1293 | |
| 1294 return positionFromPoint(x, y); | |
| 1295 } | |
| 1296 | |
| 1297 abstract CodePosition positionFromPoint(int x, int y); | |
| 1298 | |
| 1299 abstract CodePosition getPosition(int line, int column); | |
| 1300 | |
| 1301 // These are local line/column to this block | |
| 1302 abstract LineColumn getLineColumn(int offset); | |
| 1303 | |
| 1304 abstract Point offsetToPoint(int offset); | |
| 1305 | |
| 1306 abstract void _redraw(); | |
| 1307 } | |
| 1308 | |
| 1309 | |
| 1310 class KeyBindings { | |
| 1311 static final Map _remap = const { | |
| 1312 'U+001B':'Esc', 'U+0008':'Delete', 'U+0009':'Tab', 'U+0020':'Space', | |
| 1313 'Shift':'', 'Control':'', 'Alt':'', 'Meta':'' | |
| 1314 }; | |
| 1315 | |
| 1316 static String _getModifiers(event) { | |
| 1317 String ret = ''; | |
| 1318 if (event.ctrlKey) { ret += 'Control-'; } | |
| 1319 if (event.altKey) { ret += 'Alt-'; } | |
| 1320 if (event.metaKey) { ret += 'Meta-'; } | |
| 1321 if (event.shiftKey) { ret += 'Shift-'; } | |
| 1322 return ret; | |
| 1323 } | |
| 1324 | |
| 1325 // TODO(jimhug): Move this to base <= 36 and into shared code. | |
| 1326 static int _hexDigit(int c) { | |
| 1327 if(c >= 48/*0*/ && c <= 57/*9*/) { | |
| 1328 return c - 48; | |
| 1329 } else if (c >= 97/*a*/ && c <= 102/*f*/) { | |
| 1330 return c - 87; | |
| 1331 } else if (c >= 65/*A*/ && c <= 70/*F*/) { | |
| 1332 return c - 55; | |
| 1333 } else { | |
| 1334 return -1; | |
| 1335 } | |
| 1336 } | |
| 1337 | |
| 1338 static int parseHex(String hex) { | |
| 1339 var result = 0; | |
| 1340 | |
| 1341 for (int i=0; i < hex.length; i++) { | |
| 1342 var digit = _hexDigit(hex.charCodeAt(i)); | |
| 1343 assert(digit != -1); | |
| 1344 result = (result << 4) + digit; | |
| 1345 } | |
| 1346 | |
| 1347 return result; | |
| 1348 } | |
| 1349 | |
| 1350 static String translate(event) { | |
| 1351 var ret = _remap[event.keyIdentifier]; | |
| 1352 if (ret === null) ret = event.keyIdentifier; | |
| 1353 | |
| 1354 if (ret == '') { | |
| 1355 return null; | |
| 1356 } else if (ret.startsWith('U+')) { | |
| 1357 // This method only reports "non-text" key presses | |
| 1358 if (event.ctrlKey || event.altKey || event.metaKey) { | |
| 1359 return _getModifiers(event) + | |
| 1360 new String.fromCharCodes([parseHex(ret.substring(2, re
t.length))]); | |
| 1361 } else { | |
| 1362 return null; | |
| 1363 } | |
| 1364 } else { | |
| 1365 return _getModifiers(event) + ret; | |
| 1366 } | |
| 1367 } | |
| 1368 | |
| 1369 var node; | |
| 1370 Map bindings; | |
| 1371 var handleText, handleUnknown; | |
| 1372 | |
| 1373 KeyBindings(this.node, this.bindings, this.handleText, this.handleUnknown) { | |
| 1374 node.addEventListener('textInput', onTextInput, false); | |
| 1375 node.addEventListener('keydown', onKeydown, false); | |
| 1376 } | |
| 1377 | |
| 1378 onTextInput(TextEvent event) { | |
| 1379 var text = event.data; | |
| 1380 var ret; | |
| 1381 if (bindings[text] !== null) { | |
| 1382 ret = bindings[text](); | |
| 1383 } else { | |
| 1384 ret = handleText(text); | |
| 1385 } | |
| 1386 // TODO(jimhug): Unfortunate coupling to shell. | |
| 1387 shell._editor._redraw(); | |
| 1388 return ret; | |
| 1389 } | |
| 1390 | |
| 1391 // TODO(jimhug): KeyboardEvent type is needed! | |
| 1392 onKeydown(KeyboardEvent event) { | |
| 1393 final key = translate(event); | |
| 1394 if (key !== null) { | |
| 1395 if (bindings[key] !== null) { | |
| 1396 bindings[key](); | |
| 1397 event.preventDefault(); | |
| 1398 } else { | |
| 1399 if (handleUnknown(key)) { | |
| 1400 event.preventDefault(); | |
| 1401 } else { | |
| 1402 event.stopPropagation(); | |
| 1403 } | |
| 1404 } | |
| 1405 } else { | |
| 1406 event.stopPropagation(); | |
| 1407 } | |
| 1408 // TODO(jimhug): Unfortunate coupling to shell. | |
| 1409 shell._editor._redraw(); | |
| 1410 return false; | |
| 1411 } | |
| 1412 } | |
| 1413 | |
| 1414 | |
| 1415 | |
| 1416 // TODO(jimhug): Copy, paste and then modified from dartdoc | |
| 1417 /** | |
| 1418 * Kinds of tokens that we care to highlight differently. The values of the | |
| 1419 * fields here will be used as CSS class names for the generated spans. | |
| 1420 */ | |
| 1421 class Classification { | |
| 1422 static final NONE = null; | |
| 1423 static final ERROR = "e"; | |
| 1424 static final COMMENT = "c"; | |
| 1425 static final IDENTIFIER = "i"; | |
| 1426 static final KEYWORD = "k"; | |
| 1427 static final OPERATOR = "o"; | |
| 1428 static final STRING = "s"; | |
| 1429 static final NUMBER = "n"; | |
| 1430 static final PUNCTUATION = "p"; | |
| 1431 | |
| 1432 // A few things that are nice to make different: | |
| 1433 static final TYPE_IDENTIFIER = "t"; | |
| 1434 | |
| 1435 // Between a keyword and an identifier | |
| 1436 static final SPECIAL_IDENTIFIER = "r"; | |
| 1437 | |
| 1438 static final ARROW_OPERATOR = "a"; | |
| 1439 | |
| 1440 static final STRING_INTERPOLATION = 'si'; | |
| 1441 } | |
| 1442 | |
| 1443 // TODO(rnystrom): should exist in standard lib somewhere | |
| 1444 String htmlEscape(String text) { | |
| 1445 return text.replaceAll('&', '&').replaceAll( | |
| 1446 '>', '>').replaceAll('<', '<'); | |
| 1447 } | |
| 1448 | |
| 1449 bool _looksLikeType(String name) { | |
| 1450 // If the name looks like an UppercaseName, assume it's a type. | |
| 1451 return _looksLikePublicType(name) || _looksLikePrivateType(name); | |
| 1452 } | |
| 1453 | |
| 1454 bool _looksLikePublicType(String name) { | |
| 1455 // If the name looks like an UppercaseName, assume it's a type. | |
| 1456 return name.length >= 2 && isUpper(name[0]) && isLower(name[1]); | |
| 1457 } | |
| 1458 | |
| 1459 bool _looksLikePrivateType(String name) { | |
| 1460 // If the name looks like an _UppercaseName, assume it's a type. | |
| 1461 return (name.length >= 3 && name[0] == '_' && isUpper(name[1]) | |
| 1462 && isLower(name[2])); | |
| 1463 } | |
| 1464 | |
| 1465 // These ensure that they don't return "true" if the string only has symbols. | |
| 1466 bool isUpper(String s) => s.toLowerCase() != s; | |
| 1467 bool isLower(String s) => s.toUpperCase() != s; | |
| 1468 | |
| 1469 String classify(Token token) { | |
| 1470 switch (token.kind) { | |
| 1471 case TokenKind.ERROR: | |
| 1472 return Classification.ERROR; | |
| 1473 | |
| 1474 case TokenKind.IDENTIFIER: | |
| 1475 // Special case for names that look like types. | |
| 1476 if (_looksLikeType(token.text) | |
| 1477 || token.text == 'num' | |
| 1478 || token.text == 'bool' | |
| 1479 || token.text == 'int' | |
| 1480 || token.text == 'double') { | |
| 1481 return Classification.TYPE_IDENTIFIER; | |
| 1482 } | |
| 1483 return Classification.IDENTIFIER; | |
| 1484 | |
| 1485 // Even though it's a reserved word, let's try coloring it like a type. | |
| 1486 case TokenKind.VOID: | |
| 1487 return Classification.TYPE_IDENTIFIER; | |
| 1488 | |
| 1489 case TokenKind.THIS: | |
| 1490 case TokenKind.SUPER: | |
| 1491 return Classification.SPECIAL_IDENTIFIER; | |
| 1492 | |
| 1493 case TokenKind.STRING: | |
| 1494 case TokenKind.STRING_PART: | |
| 1495 case TokenKind.INCOMPLETE_STRING: | |
| 1496 case TokenKind.INCOMPLETE_MULTILINE_STRING_DQ: | |
| 1497 case TokenKind.INCOMPLETE_MULTILINE_STRING_SQ: | |
| 1498 return Classification.STRING; | |
| 1499 | |
| 1500 case TokenKind.INTEGER: | |
| 1501 case TokenKind.HEX_INTEGER: | |
| 1502 case TokenKind.DOUBLE: | |
| 1503 return Classification.NUMBER; | |
| 1504 | |
| 1505 case TokenKind.COMMENT: | |
| 1506 case TokenKind.INCOMPLETE_COMMENT: | |
| 1507 return Classification.COMMENT; | |
| 1508 | |
| 1509 // => is so awesome it is in a class of its own. | |
| 1510 case TokenKind.ARROW: | |
| 1511 return Classification.ARROW_OPERATOR; | |
| 1512 | |
| 1513 case TokenKind.HASHBANG: | |
| 1514 case TokenKind.LPAREN: | |
| 1515 case TokenKind.RPAREN: | |
| 1516 case TokenKind.LBRACK: | |
| 1517 case TokenKind.RBRACK: | |
| 1518 case TokenKind.LBRACE: | |
| 1519 case TokenKind.RBRACE: | |
| 1520 case TokenKind.COLON: | |
| 1521 case TokenKind.SEMICOLON: | |
| 1522 case TokenKind.COMMA: | |
| 1523 case TokenKind.DOT: | |
| 1524 case TokenKind.ELLIPSIS: | |
| 1525 return Classification.PUNCTUATION; | |
| 1526 | |
| 1527 case TokenKind.INCR: | |
| 1528 case TokenKind.DECR: | |
| 1529 case TokenKind.BIT_NOT: | |
| 1530 case TokenKind.NOT: | |
| 1531 case TokenKind.ASSIGN: | |
| 1532 case TokenKind.ASSIGN_OR: | |
| 1533 case TokenKind.ASSIGN_XOR: | |
| 1534 case TokenKind.ASSIGN_AND: | |
| 1535 case TokenKind.ASSIGN_SHL: | |
| 1536 case TokenKind.ASSIGN_SAR: | |
| 1537 case TokenKind.ASSIGN_SHR: | |
| 1538 case TokenKind.ASSIGN_ADD: | |
| 1539 case TokenKind.ASSIGN_SUB: | |
| 1540 case TokenKind.ASSIGN_MUL: | |
| 1541 case TokenKind.ASSIGN_DIV: | |
| 1542 case TokenKind.ASSIGN_TRUNCDIV: | |
| 1543 case TokenKind.ASSIGN_MOD: | |
| 1544 case TokenKind.CONDITIONAL: | |
| 1545 case TokenKind.OR: | |
| 1546 case TokenKind.AND: | |
| 1547 case TokenKind.BIT_OR: | |
| 1548 case TokenKind.BIT_XOR: | |
| 1549 case TokenKind.BIT_AND: | |
| 1550 case TokenKind.SHL: | |
| 1551 case TokenKind.SAR: | |
| 1552 case TokenKind.SHR: | |
| 1553 case TokenKind.ADD: | |
| 1554 case TokenKind.SUB: | |
| 1555 case TokenKind.MUL: | |
| 1556 case TokenKind.DIV: | |
| 1557 case TokenKind.TRUNCDIV: | |
| 1558 case TokenKind.MOD: | |
| 1559 case TokenKind.EQ: | |
| 1560 case TokenKind.NE: | |
| 1561 case TokenKind.EQ_STRICT: | |
| 1562 case TokenKind.NE_STRICT: | |
| 1563 case TokenKind.LT: | |
| 1564 case TokenKind.GT: | |
| 1565 case TokenKind.LTE: | |
| 1566 case TokenKind.GTE: | |
| 1567 case TokenKind.INDEX: | |
| 1568 case TokenKind.SETINDEX: | |
| 1569 return Classification.OPERATOR; | |
| 1570 | |
| 1571 // Color this like a keyword | |
| 1572 case TokenKind.HASH: | |
| 1573 | |
| 1574 case TokenKind.ABSTRACT: | |
| 1575 case TokenKind.ASSERT: | |
| 1576 case TokenKind.CLASS: | |
| 1577 case TokenKind.EXTENDS: | |
| 1578 case TokenKind.FACTORY: | |
| 1579 case TokenKind.GET: | |
| 1580 case TokenKind.IMPLEMENTS: | |
| 1581 case TokenKind.IMPORT: | |
| 1582 case TokenKind.INTERFACE: | |
| 1583 case TokenKind.LIBRARY: | |
| 1584 case TokenKind.NATIVE: | |
| 1585 case TokenKind.NEGATE: | |
| 1586 case TokenKind.OPERATOR: | |
| 1587 case TokenKind.SET: | |
| 1588 case TokenKind.SOURCE: | |
| 1589 case TokenKind.STATIC: | |
| 1590 case TokenKind.TYPEDEF: | |
| 1591 case TokenKind.BREAK: | |
| 1592 case TokenKind.CASE: | |
| 1593 case TokenKind.CATCH: | |
| 1594 case TokenKind.CONST: | |
| 1595 case TokenKind.CONTINUE: | |
| 1596 case TokenKind.DEFAULT: | |
| 1597 case TokenKind.DO: | |
| 1598 case TokenKind.ELSE: | |
| 1599 case TokenKind.FALSE: | |
| 1600 case TokenKind.FINALLY: | |
| 1601 case TokenKind.FOR: | |
| 1602 case TokenKind.IF: | |
| 1603 case TokenKind.IN: | |
| 1604 case TokenKind.IS: | |
| 1605 case TokenKind.NEW: | |
| 1606 case TokenKind.NULL: | |
| 1607 case TokenKind.RETURN: | |
| 1608 case TokenKind.SWITCH: | |
| 1609 case TokenKind.THROW: | |
| 1610 case TokenKind.TRUE: | |
| 1611 case TokenKind.TRY: | |
| 1612 case TokenKind.WHILE: | |
| 1613 case TokenKind.VAR: | |
| 1614 case TokenKind.FINAL: | |
| 1615 return Classification.KEYWORD; | |
| 1616 | |
| 1617 case TokenKind.WHITESPACE: | |
| 1618 case TokenKind.END_OF_FILE: | |
| 1619 return Classification.NONE; | |
| 1620 | |
| 1621 default: | |
| 1622 return Classification.NONE; | |
| 1623 } | |
| 1624 } | |
| OLD | NEW |