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 |