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