Reviewed by Tim H.
[WebKit-https.git] / WebKitTools / Drosera / debugger.js
1 /*
2  * Copyright (C) 2006 Apple Computer, Inc.  All rights reserved.
3  * Copyright (C) 2006 David Smith (catfish.man@gmail.com)
4  * Copyright (C) 2006 Vladimir Olexa (vladimir.olexa@gmail.com)
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  *
10  * 1.  Redistributions of source code must retain the above copyright
11  *     notice, this list of conditions and the following disclaimer. 
12  * 2.  Redistributions in binary form must reproduce the above copyright
13  *     notice, this list of conditions and the following disclaimer in the
14  *     documentation and/or other materials provided with the distribution. 
15  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
16  *     its contributors may be used to endorse or promote products derived
17  *     from this software without specific prior written permission. 
18  *
19  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
20  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
23  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 var files = new Array();
32 var filesLookup = new Object();
33 var scripts = new Array();
34 var currentFile = -1;
35 var currentRow = null;
36 var currentStack = null;
37 var currentCallFrame = null;
38 var lastStatement = null;
39 var frameLineNumberStack = new Array();
40 var previousFiles = new Array();
41 var nextFiles = new Array();
42 var isResizingColumn = false;
43 var draggingBreakpoint = null;
44 var steppingOut = false;
45 var steppingOver = false;
46 var steppingStack = 0;
47 var pauseOnNextStatement = false;
48 var pausedWhileLeavingFrame = false;
49 var consoleWindow = null;
50 var enabledBreakpoint = "break";
51
52 ScriptCallFrame = function (functionName, index, row)
53 {
54     this.functionName = functionName;
55     this.index = index;
56     this.row = row;
57     this.localVariableNames = null;
58 }
59
60 ScriptCallFrame.prototype.valueForScopeVariable = function (name)
61 {
62     return DebuggerDocument.valueForScopeVariableNamed_inCallFrame_(name, this.index);
63 }
64
65 ScriptCallFrame.prototype.loadVariables = function ()
66 {
67     if (!this.localVariableNames)
68         this.localVariableNames = DebuggerDocument.localScopeVariableNamesForCallFrame_(this.index);
69
70     var variablesTable = document.getElementById("variablesTable");
71     variablesTable.innerHTML = "";
72
73     for(var i = 0; i < this.localVariableNames.length; i++) {
74         var tr = document.createElement("tr");
75         var td = document.createElement("td");
76         td.innerText = this.localVariableNames[i];
77         td.className = "variable";
78         tr.appendChild(td);
79
80         td = document.createElement("td");
81         td.innerText = this.valueForScopeVariable(this.localVariableNames[i]);
82         tr.appendChild(td);
83         tr.addEventListener("click", selectVariable, true);
84
85         variablesTable.appendChild(tr);
86     }
87 }
88
89 function sleep(numberMillis) {
90     var now = new Date();
91     var exitTime = now.getTime() + numberMillis;
92     while (true) {
93         now = new Date();
94         if (now.getTime() > exitTime)
95             return;
96     }
97 }
98
99 function headerMouseDown(element) {
100     if (!isResizingColumn) 
101         element.style.background = "url(glossyHeaderPressed.png) repeat-x";
102 }
103
104 function headerMouseUp(element) {
105     element.style.background = "url(glossyHeader.png) repeat-x";
106 }
107
108 function headerMouseOut(element) {
109     element.style.background = "url(glossyHeader.png) repeat-x";
110 }
111
112 function filesDividerDragStart(event) 
113 {
114     dividerDragStart(document.getElementById("filesDivider"), filesDividerDrag, filesDividerDragEnd, event, "col-resize");
115 }
116
117 function filesDividerDragEnd(event) 
118 {
119     dividerDragEnd(document.getElementById("filesDivider"), filesDividerDrag, filesDividerDragEnd, event);
120 }
121
122 function filesDividerDrag(event) 
123 {
124     var element = document.getElementById("filesDivider");
125     if (document.getElementById("filesDivider").dragging == true) {
126         var masterMain = document.getElementById("masterMain");
127         var main = document.getElementById("main");
128         var fileBrowser = document.getElementById("fileBrowser");
129         var x = event.clientX + window.scrollX;
130         var delta = element.dragLastX - x;
131         var newWidth = constrainedWidthFromElement(fileBrowser.clientWidth - delta, masterMain, 0.1, 0.9);
132         if ((fileBrowser.clientWidth - delta) == newWidth) // the width wasn't constrained
133             element.dragLastX = x;
134         fileBrowser.style.width = newWidth + "px";
135         main.style.left = newWidth + "px";
136         event.preventDefault();
137     }
138 }
139
140 function dividerDragStart(element, dividerDrag, dividerDragEnd, event, cursor) {
141     element.dragging = true;
142     element.dragLastY = event.clientY + window.scrollY;
143     element.dragLastX = event.clientX + window.scrollX;
144     document.addEventListener("mousemove", dividerDrag, true);
145     document.addEventListener("mouseup", dividerDragEnd, true);
146     document.body.style.cursor = cursor;
147     event.preventDefault();
148 }
149
150 function dividerDragEnd(element, dividerDrag, dividerDragEnd, event) {
151     element.dragging = false;
152     document.removeEventListener("mousemove", dividerDrag, true);
153     document.removeEventListener("mouseup", dividerDragEnd, true);
154     document.body.style.cursor = null;
155 }
156
157 function dividerDrag(event) {
158     var element = document.getElementById("divider");
159     if (document.getElementById("divider").dragging == true) {
160         var main = document.getElementById("main");
161         var top = document.getElementById("info");
162         var bottom = document.getElementById("body");
163         var y = event.clientY + window.scrollY;
164         var delta = element.dragLastY - y;
165         var newHeight = constrainedHeightFromElement(top.clientHeight - delta, main);
166         if ((top.clientHeight - delta) == newHeight) // the height wasn't constrained
167             element.dragLastY = y;
168         top.style.height = newHeight + "px";
169         bottom.style.top = newHeight + "px";
170         event.preventDefault();
171     }
172 }
173
174 function sourceDividerDragStart(event) {
175     dividerDragStart(document.getElementById("divider"), dividerDrag, sourceDividerDragEnd, event, "row-resize");
176 }
177
178 function sourceDividerDragEnd(event) {
179     dividerDragEnd(document.getElementById("divider"), dividerDrag, sourceDividerDragEnd, event);
180 }
181
182 function infoDividerDragStart(event) {
183     dividerDragStart(document.getElementById("infoDivider"), infoDividerDrag, infoDividerDragEnd, event, "col-resize");
184 }
185
186 function infoDividerDragEnd(event) {
187     dividerDragEnd(document.getElementById("infoDivider"), infoDividerDrag, infoDividerDragEnd, event);
188 }
189
190 function infoDividerDrag(event) {
191     var element = document.getElementById("infoDivider");
192     if (document.getElementById("infoDivider").dragging == true) {
193         var main = document.getElementById("main");
194         var leftPane = document.getElementById("leftPane");
195         var rightPane = document.getElementById("rightPane");
196         var x = event.clientX + window.scrollX;
197         var delta = element.dragLastX - x;
198         var newWidth = constrainedWidthFromElement(leftPane.clientWidth - delta, main);
199         if ((leftPane.clientWidth - delta) == newWidth) // the width wasn't constrained
200             element.dragLastX = x;
201         leftPane.style.width = newWidth + "px";
202         rightPane.style.left = newWidth + "px";
203         event.preventDefault();
204     }
205 }
206
207 function columnResizerDragStart(event) {
208     isResizingColumn = true;
209     dividerDragStart(document.getElementById("variableColumnResizer"), columnResizerDrag, columnResizerDragEnd, event, "col-resize");
210 }
211
212 function columnResizerDragEnd(event) {
213     isResizingColumn = false;
214     dividerDragEnd(document.getElementById("variableColumnResizer"), columnResizerDrag, columnResizerDragEnd, event);
215 }
216
217 function columnResizerDrag(event) {
218     var element = document.getElementById("variableColumnResizer");
219     if (element.dragging == true) {
220         var main = document.getElementById("rightPane");
221         var variableColumn = document.getElementById("variable");
222         var rules = document.defaultView.getMatchedCSSRules(variableColumn, "");
223         for (var i = 0; i < rules.length; i++) {
224             if (rules[i].selectorText == ".variable") {
225                 var columnRule = rules[i];
226                 break;
227             }
228         }
229
230         var x = event.clientX + window.scrollX;
231         var delta = element.dragLastX - x;
232         var newWidth = constrainedWidthFromElement(variableColumn.clientWidth - delta, main);
233         if ((variableColumn.clientWidth - delta) == newWidth) // the width wasn't constrained
234             element.dragLastX = x;
235         columnRule.style.width = newWidth + "px";
236         element.style.left = newWidth + "px";
237         event.preventDefault();
238     }
239 }
240
241 function constrainedWidthFromElement(width, element, constrainLeft, constrainRight) {
242     if (constrainLeft === undefined) constrainLeft = 0.25;
243     if (constrainRight === undefined) constrainRight = 0.75;
244     
245     if (width < element.clientWidth * constrainLeft)
246         width = element.clientWidth * constrainLeft;
247     else if (width > element.clientWidth * constrainRight)
248         width = element.clientWidth * constrainRight;
249     return width;
250 }
251
252 function constrainedHeightFromElement(height, element) {
253     if (height < element.clientHeight * 0.25)
254         height = element.clientHeight * 0.25;
255     else if (height > element.clientHeight * 0.75)
256         height = element.clientHeight * 0.75;
257     return height;
258 }
259
260 function loaded() {
261     document.getElementById("divider").addEventListener("mousedown", sourceDividerDragStart, false);
262     document.getElementById("infoDivider").addEventListener("mousedown", infoDividerDragStart, false);
263     document.getElementById("filesDivider").addEventListener("mousedown", filesDividerDragStart, false);
264     document.getElementById("variableColumnResizer").addEventListener("mousedown", columnResizerDragStart, false);
265 }
266
267 function isPaused() {
268     return DebuggerDocument.isPaused();
269 }
270
271 function pause() {
272     DebuggerDocument.pause();
273 }
274
275 function resume()
276 {
277     if (currentRow) {
278         currentRow.removeStyleClass("current");
279         currentRow = null;
280     }
281
282     var stackframeTable = document.getElementById("stackframeTable");
283     stackframeTable.innerHTML = ""; // clear the content
284     var variablesTable = document.getElementById("variablesTable");
285     variablesTable.innerHTML = ""; // clear the content
286     currentStack = null;
287     currentCallFrame = null;
288
289     pauseOnNextStatement = false;
290     pausedWhileLeavingFrame = false;
291     steppingOut = false;
292     steppingOver = false;
293     steppingStack = 0;
294
295     DebuggerDocument.resume();
296 }
297
298 function stepInto()
299 {
300     pauseOnNextStatement = false;
301     steppingOut = false;
302     steppingOver = false;
303     steppingStack = 0;
304     DebuggerDocument.stepInto();
305 }
306
307 function stepOver()
308 {
309     pauseOnNextStatement = false;
310     steppingOver = true;
311     steppingStack = 0;
312     DebuggerDocument.resume();
313 }
314
315 function stepOut()
316 {
317     pauseOnNextStatement = pausedWhileLeavingFrame;
318     steppingOver = false;
319     steppingStack = 0;
320     steppingOut = true;
321     DebuggerDocument.resume();
322 }
323
324 Element.prototype.removeStyleClass = function(className) {
325     if(this.hasStyleClass(className))
326         this.className = this.className.replace(className, "");
327 }
328
329 Element.prototype.addStyleClass = function(className) {
330     if (!this.hasStyleClass(className))
331         this.className += (this.className.length ? " " + className : className);
332 }
333
334 Element.prototype.hasStyleClass = function(className) {
335     return this.className.indexOf(className) != -1;
336 };
337
338 function addBreakPoint(event)
339 {
340     var row = event.target.parentNode;
341     var file = files[currentFile];
342     var lineNum = parseInt(event.target.title);
343     
344     if (row.hasStyleClass("breakpoint")) {
345         if(event.altKey && file.breakpoints[lineNum] != null)
346             showBreakpointEditor(file, lineNum);
347         else
348             toggleBreakpoint(row, file, lineNum);    
349     } else
350         createBreakpoint(row, file, lineNum);
351 }
352
353 function createBreakpoint(row, file, lineNum)
354 {
355     row.addStyleClass("breakpoint");
356     row.removeStyleClass("disabled");
357     file.breakpoints[lineNum] = enabledBreakpoint;
358     file.disabledBreakpoints[lineNum] = null;
359 }
360
361 function showBreakpointEditor(file, lineNum)
362 {
363     var editors = file.breakpointEditorWindows;
364
365     if (editors[lineNum] == null)
366         editors[lineNum] = window.open("breakpointEditor.html", ("Edit Breakpoint On Line " + lineNum), "top=200, left=200, width=400, height=200, toolbar=no, resizable=yes");
367     else
368         editors[lineNum].focus();
369         
370     event.preventDefault();
371 }
372
373 function toggleBreakpoint(row, file, lineNum)
374 {
375     if (row.hasStyleClass("disabled"))
376         row.removeStyleClass("disabled");    
377     else
378         row.addStyleClass("disabled");
379     
380     var temp = file.breakpoints[lineNum];
381     file.breakpoints[lineNum] = file.disabledBreakpoints[lineNum];
382     file.disabledBreakpoints[lineNum] = temp;
383 }
384
385 function finishEditingBreakpoint(line, file, newFunction)
386 {
387     files[file].breakpoints[line] = newFunction;
388     files[file].breakpointEditorWindows[line] = null;
389 }
390
391 function moveBreakPoint(event)
392 {
393     if (event.target.parentNode.hasStyleClass("breakpoint")) {
394         draggingBreakpoint = event.target;
395         draggingBreakpoint.started = false;
396         draggingBreakpoint.dragLastY = event.clientY + window.scrollY;
397         draggingBreakpoint.dragLastX = event.clientX + window.scrollX;
398         var sourcesDocument = document.getElementById("sources").contentDocument;
399         sourcesDocument.addEventListener("mousemove", breakpointDrag, true);
400         sourcesDocument.addEventListener("mouseup", breakpointDragEnd, true);
401         sourcesDocument.body.style.cursor = "default";
402     }
403 }
404
405 function breakpointDrag(event)
406 {
407     var sourcesDocument = document.getElementById("sources").contentDocument;
408     if (!draggingBreakpoint) {
409         sourcesDocument.removeEventListener("mousemove", breakpointDrag, true);
410         sourcesDocument.removeEventListener("mouseup", breakpointDragEnd, true);
411         sourcesDocument.body.style.cursor = null;
412         return;
413     }
414
415     var x = event.clientX + window.scrollX;
416     var y = event.clientY + window.scrollY;
417     var deltaX = draggingBreakpoint.dragLastX - x;
418     var deltaY = draggingBreakpoint.dragLastY - y;
419     if (draggingBreakpoint.started || deltaX > 4 || deltaY > 4 || deltaX < -4 || deltaY < -4) {
420     
421         if (!draggingBreakpoint.started) {
422             var node = draggingBreakpoint.parentNode;
423             draggingBreakpoint.isDisabled = node.hasStyleClass("disabled");
424             node.removeStyleClass("breakpoint");
425             node.removeStyleClass("disabled");
426             draggingBreakpoint.started = true;
427             
428             var lineNum = parseInt(draggingBreakpoint.title);
429             
430             if(draggingBreakpoint.isDisabled)
431                 draggingBreakpoint.breakFunction = files[currentFile].disabledBreakpoints[lineNum];
432             else
433                 draggingBreakpoint.breakFunction = files[currentFile].breakpoints[lineNum];
434             files[currentFile].breakpoints[lineNum] = null;
435             files[currentFile].disabledBreakpoints[lineNum] = null;
436
437             var dragImage = sourcesDocument.createElement("img");
438             if (draggingBreakpoint.isDisabled)
439                 dragImage.src = "breakPointDisabled.tif";
440             else
441                 dragImage.src = "breakPoint.tif";
442             dragImage.id = "breakpointDrag";
443             dragImage.style.top = y - 8 + "px";
444             dragImage.style.left = x - 12 + "px";
445             sourcesDocument.body.appendChild(dragImage);
446         } else {
447             var dragImage = sourcesDocument.getElementById("breakpointDrag");
448             if (!dragImage) {
449                 sourcesDocument.removeEventListener("mousemove", breakpointDrag, true);
450                 sourcesDocument.removeEventListener("mouseup", breakpointDragEnd, true);
451                 sourcesDocument.body.style.cursor = null;
452                 return;
453             }
454
455             dragImage.style.top = y - 8 + "px";
456             dragImage.style.left = x - 12 + "px";
457             if (x > 40)
458                 dragImage.style.visibility = "hidden";
459             else
460                 dragImage.style.visibility = null;
461         }
462
463         draggingBreakpoint.dragLastX = x;
464         draggingBreakpoint.dragLastY = y;
465     }
466 }
467
468 function breakpointDragEnd(event)
469 {
470     var sourcesDocument = document.getElementById("sources").contentDocument;
471     sourcesDocument.removeEventListener("mousemove", breakpointDrag, true);
472     sourcesDocument.removeEventListener("mouseup", breakpointDragEnd, true);
473     sourcesDocument.body.style.cursor = null;
474
475     var dragImage = sourcesDocument.getElementById("breakpointDrag");
476     if (!dragImage)
477         return;
478         
479     dragImage.parentNode.removeChild(dragImage);
480
481     var x = event.clientX + window.scrollX;
482     if (x > 40 || !draggingBreakpoint)
483         return;
484
485     var y = event.clientY + window.scrollY;
486     var rowHeight = draggingBreakpoint.parentNode.offsetHeight;
487     var row = Math.ceil(y / rowHeight);
488     if (row <= 0)
489         row = 1;
490
491     var file = files[currentFile];
492     var table = file.element.firstChild;
493     if (row > table.childNodes.length)
494         return;
495
496     var tr = table.childNodes.item(row - 1);
497     if (!tr)
498         return;
499         
500     if(draggingBreakpoint.isDisabled) {
501         tr.addStyleClass("disabled");
502         file.disabledBreakpoints[row] = draggingBreakpoint.breakFunction;
503         file.breakpoints[row] = null;
504     } else {
505         file.disabledBreakpoints[row] = null;
506         file.breakpoints[row] = draggingBreakpoint.breakFunction;
507     }
508
509     tr.addStyleClass("breakpoint");
510     
511     draggingBreakpoint = null;
512 }
513
514 function totalOffsetTop(element, stop)
515 {
516     var currentTop = 0;
517     while (element.offsetParent) {
518         currentTop += element.offsetTop
519         element = element.offsetParent;
520         if (element == stop)
521             break;
522     }
523     return currentTop;
524 }
525
526 function switchFile()
527 {
528     var filesSelect = document.getElementById("files");
529     fileClicked(filesSelect.options[filesSelect.selectedIndex].value, false);
530     loadFile(filesSelect.options[filesSelect.selectedIndex].value, true);
531 }
532
533 function syntaxHighlight(code)
534 {
535     var keywords = { 'abstract': 1, 'boolean': 1, 'break': 1, 'byte': 1, 'case': 1, 'catch': 1, 'char': 1, 'class': 1, 'const': 1, 'continue': 1, 'debugger': 1, 'default': 1, 'delete': 1, 'do': 1, 'double': 1, 'else': 1, 'enum': 1, 'export': 1, 'extends': 1, 'false': 1, 'final': 1, 'finally': 1, 'float': 1, 'for': 1, 'function': 1, 'goto': 1, 'if': 1, 'implements': 1, 'import': 1, 'in': 1, 'instanceof': 1, 'int': 1, 'interface': 1, 'long': 1, 'native': 1, 'new': 1, 'null': 1, 'package': 1, 'private': 1, 'protected': 1, 'public': 1, 'return': 1, 'short': 1, 'static': 1, 'super': 1, 'switch': 1, 'synchronized': 1, 'this': 1, 'throw': 1, 'throws': 1, 'transient': 1, 'true': 1, 'try': 1, 'typeof': 1, 'var': 1, 'void': 1, 'volatile': 1, 'while': 1, 'with': 1 };
536
537     function echoChar(c) {
538         if (c == '<')
539             result += '&lt;';
540         else if (c == '>')
541             result += '&gt;';
542         else if (c == '&')
543             result += '&amp;';
544         else if (c == '\t')
545             result += '    ';
546         else
547             result += c;
548     }
549
550     function isDigit(number) {
551         var string = "1234567890";
552         if (string.indexOf(number) != -1)
553             return true;
554         return false;
555     }
556
557     function isHex(hex) {
558         var string = "1234567890abcdefABCDEF";
559         if (string.indexOf(hex) != -1)
560             return true;
561         return false;
562     }
563
564     function isLetter(letter) {
565         var string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
566         if (string.indexOf(letter) != -1)
567             return true;
568         return false;
569     }
570
571     var result = "";
572     var cPrev = "";
573     var c = "";
574     var cNext = "";
575     for (var i = 0; i < code.length; i++) {
576         cPrev = c;
577         c = code.charAt(i);
578         cNext = code.charAt(i + 1);
579
580         if (c == "/" && cNext == "*") {
581             result += "<span class=\"comment\">";
582             echoChar(c);
583             echoChar(cNext);
584             for (i += 2; i < code.length; i++) {
585                 c = code.charAt(i);
586                 if (c == "\n")
587                     result += "</span>";
588                 echoChar(c);
589                 if (c == "\n")
590                     result += "<span class=\"comment\">";
591                 if (cPrev == "*" && c == "/")
592                     break;
593                 cPrev = c;
594             }
595             result += "</span>";
596             continue;
597         } else if (c == "/" && cNext == "/") {
598             result += "<span class=\"comment\">";
599             echoChar(c);
600             echoChar(cNext);
601             for (i += 2; i < code.length; i++) {
602                 c = code.charAt(i);
603                 if (c == "\n")
604                     break;
605                 echoChar(c);
606             }
607             result += "</span>";
608             echoChar(c);
609             continue;
610         } else if (c == "\"" || c == "'") {
611             var instringtype = c;
612             var stringstart = i;
613             result += "<span class=\"string\">";
614             echoChar(c);
615             for (i += 1; i < code.length; i++) {
616                 c = code.charAt(i);
617                 if (stringstart < (i - 1) && cPrev == instringtype && code.charAt(i - 2) != "\\")
618                     break;
619                 echoChar(c);
620                 cPrev = c;
621             }
622             result += "</span>";
623             echoChar(c);
624             continue;
625         } else if (c == "0" && cNext == "x" && (i == 0 || (!isLetter(cPrev) && !isDigit(cPrev)))) {
626             result += "<span class=\"number\">";
627             echoChar(c);
628             echoChar(cNext);
629             for (i += 2; i < code.length; i++) {
630                 c = code.charAt(i);
631                 if (!isHex(c))
632                     break;
633                 echoChar(c);
634             }
635             result += "</span>";
636             echoChar(c);
637             continue;
638         } else if ((isDigit(c) || ((c == "-" || c == ".") && isDigit(cNext))) && (i == 0 || (!isLetter(cPrev) && !isDigit(cPrev)))) {
639             result += "<span class=\"number\">";
640             echoChar(c);
641             for (i += 1; i < code.length; i++) {
642                 c = code.charAt(i);
643                 if (!isDigit(c) && c != ".")
644                     break;
645                 echoChar(c);
646             }
647             result += "</span>";
648             echoChar(c);
649             continue;
650         } else if(isLetter(c) && (i == 0 || !isLetter(cPrev))) {
651             var keyword = c;
652             var cj = "";
653             for (var j = i + 1; j < i + 12 && j < code.length; j++) {
654                 cj = code.charAt(j);
655                 if (!isLetter(cj))
656                     break;
657                 keyword += cj;
658             }
659
660             if (keywords[keyword]) {
661                 result += "<span class=\"keyword\">" + keyword + "</span>";
662                 i += keyword.length - 1;
663                 continue;
664             }
665         }
666
667         echoChar(c);
668     }
669
670     return result;
671 }
672
673 function navFilePrevious(element)
674 {
675     if (element.disabled)
676         return;
677     var lastFile = previousFiles.pop();
678     if (currentFile != -1)
679         nextFiles.unshift(currentFile);
680     loadFile(lastFile, false);
681 }
682
683 function navFileNext(element)
684 {
685     if (element.disabled)
686         return;
687     var lastFile = nextFiles.shift();
688     if (currentFile != -1)
689         previousFiles.push(currentFile);
690     loadFile(lastFile, false);
691 }
692
693 function updateFunctionStack()
694 {
695     var stackframeTable = document.getElementById("stackframeTable");
696     stackframeTable.innerHTML = ""; // clear the content
697
698     currentStack = new Array();
699     var stack = DebuggerDocument.currentFunctionStack();
700     for(var i = 0; i < stack.length; i++) {
701         var tr = document.createElement("tr");
702         var td = document.createElement("td");
703         td.className = "stackNumber";
704         td.innerText = i;
705         tr.appendChild(td);
706
707         td = document.createElement("td");
708         td.innerText = stack[i];
709         tr.appendChild(td);
710         tr.addEventListener("click", selectStackFrame, true);
711
712         stackframeTable.appendChild(tr);
713
714         var frame = new ScriptCallFrame(stack[i], i, tr);
715         tr.callFrame = frame;
716         currentStack.push(frame);
717
718         if (i == 0) {
719             tr.addStyleClass("current");
720             frame.loadVariables();
721             currentCallFrame = frame;
722         }
723     }
724 }
725
726 function selectStackFrame(event)
727 {
728     var stackframeTable = document.getElementById("stackframeTable");
729     var rows = stackframeTable.childNodes;
730     for (var i = 0; i < rows.length; i++)
731         rows[i].removeStyleClass("current");
732     this.addStyleClass("current");
733     this.callFrame.loadVariables();
734     currentCallFrame = this.callFrame;
735
736     if (frameLineNumberInfo = frameLineNumberStack[this.callFrame.index - 1])
737         jumpToLine(frameLineNumberInfo[0], frameLineNumberInfo[1]);
738     else if (this.callFrame.index == 0)
739         jumpToLine(lastStatement[0], lastStatement[1]);
740 }
741
742 function selectVariable(event)
743 {
744     var variablesTable = document.getElementById("variablesTable");
745     var rows = variablesTable.childNodes;
746     for (var i = 0; i < rows.length; i++)
747         rows[i].removeStyleClass("current");
748     this.addStyleClass("current");
749 }
750
751 function loadFile(fileIndex, manageNavLists)
752 {
753     var file = files[fileIndex];
754     if (!file)
755         return;
756
757     if (currentFile != -1 && files[currentFile] && files[currentFile].element)
758         files[currentFile].element.style.display = "none";
759
760     if (!file.loaded) {
761         var sourcesDocument = document.getElementById("sources").contentDocument;
762         var sourcesDiv = sourcesDocument.body;
763         var sourceDiv = sourcesDocument.createElement("div");
764         sourceDiv.id = "file" + fileIndex;
765         sourcesDiv.appendChild(sourceDiv);
766         file.element = sourceDiv;
767
768         var table = sourcesDocument.createElement("table");
769         sourceDiv.appendChild(table);
770
771         var normalizedSource = file.source.replace(/\r\n|\r/, "\n"); // normalize line endings
772         var lines = syntaxHighlight(normalizedSource).split("\n");
773         for( var i = 0; i < lines.length; i++ ) {
774             var tr = sourcesDocument.createElement("tr");
775             var td = sourcesDocument.createElement("td");
776             td.className = "gutter";
777             td.title = (i + 1);
778             td.addEventListener("click", addBreakPoint, true);
779             td.addEventListener("mousedown", moveBreakPoint, true);
780             tr.appendChild(td);
781
782             td = sourcesDocument.createElement("td");
783             td.className = "source";
784             td.innerHTML = (lines[i].length ? lines[i] : "&nbsp;");
785             tr.appendChild(td);
786             table.appendChild(tr);
787         }
788
789         file.loaded = true;
790     }
791
792     file.element.style.display = null;
793
794     document.getElementById("filesPopupButtonContent").innerText = (file.url ? file.url : "(unknown script)");
795     
796     var filesSelect = document.getElementById("files");
797     for (var i = 0; i < filesSelect.childNodes.length; i++) {
798         if (filesSelect.childNodes[i].value == fileIndex) {
799             filesSelect.selectedIndex = i;
800             break;
801         }
802     }
803     
804     
805     if (manageNavLists) {
806         nextFiles = new Array();
807         if (currentFile != -1)
808             previousFiles.push(currentFile);
809     }
810
811     document.getElementById("navFileLeftButton").disabled = (previousFiles.length == 0);
812     document.getElementById("navFileRightButton").disabled = (nextFiles.length == 0);
813
814     currentFile = fileIndex;
815 }
816
817 function updateFileSource(source, url, force)
818 {
819     var fileIndex = filesLookup[url];
820     if (!fileIndex || !source.length)
821         return;
822
823     var file = files[fileIndex];
824     if (force || file.source.length != source.length || file.source != source) {
825         file.source = source;
826         file.loaded = false;
827
828         if (file.element) {
829             file.element.parentNode.removeChild(file.element);
830             file.element = null;
831         }
832
833         if (currentFile == fileIndex)
834             loadFile(fileIndex, false);
835     }
836 }
837
838 /**
839 * ParsedURL - this object chops up full URL into two parts: 
840  * 1) The domain: everything from http:// to the end of the domain name
841  * 2) The relative path: everything after the domain
842  *
843  * @param string url URL to be processed
844  */
845 function ParsedURL(url)
846 {
847     // Since we're getting the URL from the browser, we're safe to assume the URL is already well formatted
848     // and so there is no need for more sophisticated regular expression here
849     var url_parts = url.match(/(http[s]?:\/\/(www)?\.?(\w|\.|-)+\w(:\d{1,5})?)\/?(.*)/);
850     
851     // the domain here is considered the whole http://www.example.org:8000 string for display purposes
852     this.domain = url_parts[1];
853     // the relative path is everything following the domain
854     this.relativePath = "/" + url_parts[5];
855 }
856
857 /**
858 * SiteBrowser - modifies the file tree via DOM as new files are being open
859  *
860  */
861 function SiteBrowser()
862 {
863     var fileBrowser = document.getElementById("filesBrowserSites");
864     
865     this.addURL = function add(url, fileIndex)
866     {
867         var parsedURL = new ParsedURL(url);
868         var divs = fileBrowser.getElementsByTagName("div");
869         
870         if (divs.length == 0) { 
871             addNewDomain(parsedURL, fileIndex);
872         } else {
873             var isNew = true;
874             for (var i = 0; i < divs.length; i++) {
875                 if (divs[i].id == parsedURL.domain) {
876                     var uls = divs[i].getElementsByTagName("ul");
877                     var ul = (uls.length > 0) ? uls[0] : document.createElement("ul");
878                     var li = document.createElement("li");
879                     
880                     li.id = fileIndex;
881                     li.addEventListener("click", fileBrowserMouseEvents, false);
882                     li.title = li.innerText = parsedURL.relativePath ? parsedURL.relativePath : "/";
883                     ul.appendChild(li);
884                     isNew = false;
885                     break;
886                 }
887             }
888             if (isNew) {
889                 addNewDomain(parsedURL, fileIndex);
890             }
891         }
892     }
893     
894     this.selectInitialFile = function sf()
895     {
896         if (currentFile == -1)
897             document.getElementById("1").className = "active";
898     }
899     
900     function addNewDomain(parsedURL, fileIndex)
901     {
902         var div = document.createElement("div");
903         var ul = document.createElement("ul");
904         var li = document.createElement("li");
905         
906         div.id = div.innerText = div.title = parsedURL.domain;
907         div.addEventListener("click", fileBrowserMouseEvents, false);
908         // Maybe we can add some roll-overs here...
909         //div.addEventListener("mouseover", fileBrowserMouseEvents, false);
910         //div.addEventListener("mouseout", fileBrowserMouseEvents, false);
911         li.id = fileIndex;
912         li.addEventListener("click", fileBrowserMouseEvents, false);
913         li.title = li.innerText = parsedURL.relativePath ? parsedURL.relativePath : "/";
914         ul.appendChild(li);
915         div.appendChild(ul);
916         fileBrowser.appendChild(div);        
917     }
918 }
919
920 function fileBrowserMouseEvents(event)
921 {
922     switch (event.type)
923     {
924         case "click":
925             // If we clicked on a site, collapse/expand it, if on a file, display it. Since we're only capturing this
926             // event from either a DIV or LI element, we don't have to worry about any ambiguity
927             (event.target.nodeName.toUpperCase() == "DIV") ? toggleCollapseSite(event) : fileClicked(event.target.id);
928             break;
929     }
930 }
931
932 function fileClicked(fileId, shouldLoadFile)
933 {
934     if (shouldLoadFile === undefined) shouldLoadFile = true;
935     
936     document.getElementById(currentFile).className  = "passive";
937     document.getElementById(fileId).className       = "active";
938     if (shouldLoadFile) 
939         loadFile(fileId, false);
940 }
941
942 function toggleCollapseSite(event)
943 {
944     var thisSite = document.getElementById(event.target.id);
945     var siteFiles = thisSite.getElementsByTagName("ul");
946     
947     if (siteFiles[0].style.display == "block" || !siteFiles[0].style.display) {
948         siteFiles[0].style.display = "none";
949         thisSite.className = "collapsed"; 
950     } else {
951         siteFiles[0].style.display = "block";
952         thisSite.className = "expanded";
953     }
954 }
955
956 function didParseScript(source, fileSource, url, sourceId, baseLineNumber)
957 {
958     var fileIndex = filesLookup[url];
959     var file = files[fileIndex];
960     var firstLoad = false;
961
962     if (!fileIndex || !file) {
963         fileIndex = files.length + 1;
964         if (url.length)
965             filesLookup[url] = fileIndex;
966
967         file = new Object();
968         file.scripts = new Array();
969         file.breakpoints = new Array();
970         file.disabledBreakpoints = new Array();
971         file.breakpointEditorWindows = new Array();
972         file.source = (fileSource.length ? fileSource : source);
973         file.url = (url.length ? url : null);
974         file.loaded = false;
975
976         files[fileIndex] = file;
977
978         var filesSelect = document.getElementById("files");
979         var option = document.createElement("option");
980         files[fileIndex].menuOption = option;
981         option.value = fileIndex;
982         option.text = (file.url ? file.url : "(unknown script)");
983         filesSelect.appendChild(option);
984
985         var siteBrowser = new SiteBrowser();
986         siteBrowser.addURL(file.url, fileIndex);
987         siteBrowser.selectInitialFile();        
988         
989         firstLoad = true;
990     }
991
992     var sourceObj = new Object();
993     sourceObj.file = fileIndex;
994     sourceObj.baseLineNumber = baseLineNumber;
995     file.scripts.push(sourceId);
996     scripts[sourceId] = sourceObj;
997
998     if (!firstLoad)
999         updateFileSource((fileSource.length ? fileSource : source), url, false);
1000
1001     if (currentFile == -1)
1002         loadFile(fileIndex, false);
1003 }
1004
1005 function jumpToLine(sourceId, line)
1006 {
1007     var script = scripts[sourceId];
1008     if (line <= 0 || !script)
1009         return;
1010
1011     var file = files[script.file];
1012     if (!file)
1013         return;
1014
1015     if (currentFile != script.file)
1016         loadFile(script.file, true);
1017     if (currentRow)
1018         currentRow.removeStyleClass("current");
1019     if (!file.element)
1020         return;
1021     if (line > file.element.firstChild.childNodes.length)
1022         return;
1023
1024     currentRow = file.element.firstChild.childNodes.item(line - 1);
1025     if (!currentRow)
1026         return;
1027
1028     currentRow.addStyleClass("current");
1029
1030     var sourcesDiv = document.getElementById("sources");
1031     var sourcesDocument = document.getElementById("sources").contentDocument;
1032     var parent = sourcesDocument.body;
1033     var offset = totalOffsetTop(currentRow, parent);
1034     if (offset < (parent.scrollTop + 20) || offset > (parent.scrollTop + sourcesDiv.clientHeight - 20))
1035         parent.scrollTop = totalOffsetTop(currentRow, parent) - (sourcesDiv.clientHeight / 2) + 10;
1036 }
1037
1038 function willExecuteStatement(sourceId, line, fromLeavingFrame)
1039 {
1040     var script = scripts[sourceId];
1041     if (line <= 0 || !script)
1042         return;
1043
1044     var file = files[script.file];
1045     if (!file)
1046         return;
1047
1048     lastStatement = [sourceId, line];
1049     
1050     var breakpoint = file.breakpoints[line];
1051     var shouldBreak = breakpoint && (breakpoint == "break" || DebuggerDocument.evaluateScript_inCallFrame_(breakpoint, 0) == 1);
1052     
1053     if (pauseOnNextStatement || shouldBreak || (steppingOver && !steppingStack)) {
1054         pause();
1055         pauseOnNextStatement = false;
1056         pausedWhileLeavingFrame = fromLeavingFrame || false;
1057     }
1058
1059     if (isPaused()) {
1060         updateFunctionStack();
1061         jumpToLine(sourceId, line);
1062     }
1063 }
1064
1065 function didEnterCallFrame(sourceId, line)
1066 {
1067     if (steppingOver || steppingOut)
1068         steppingStack++;
1069
1070     if (lastStatement)
1071         frameLineNumberStack.unshift(lastStatement);
1072     willExecuteStatement(sourceId, line);
1073 }
1074
1075 function willLeaveCallFrame(sourceId, line)
1076 {
1077     if (line <= 0)
1078         resume();
1079     willExecuteStatement(sourceId, line, true);
1080     frameLineNumberStack.shift();
1081     if (!steppingStack)
1082         steppingOver = false;
1083     if (steppingOut && !steppingStack) {
1084         steppingOut = false;
1085         pauseOnNextStatement = true;
1086     }
1087     if ((steppingOver || steppingOut) && steppingStack >= 1)
1088         steppingStack--;
1089 }
1090
1091 function exceptionWasRaised(sourceId, line)
1092 {
1093     pause();
1094     updateFunctionStack();
1095     jumpToLine(sourceId, line);
1096 }
1097
1098 function showConsoleWindow()
1099 {
1100     if (!consoleWindow)
1101         consoleWindow = window.open("console.html", "console", "top=200, left=200, width=500, height=300, toolbar=yes, resizable=yes");
1102 }