Fixes a crash when stepping out in the Inspector's debugger.
[WebKit-https.git] / WebCore / page / inspector / ScriptsPanel.js
1 /*
2  * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WebInspector.ScriptsPanel = function()
27 {
28     WebInspector.Panel.call(this);
29
30     this.element.addStyleClass("scripts");
31
32     this.topStatusBar = document.createElement("div");
33     this.topStatusBar.className = "status-bar";
34     this.topStatusBar.id = "scripts-status-bar";
35     this.element.appendChild(this.topStatusBar);
36
37     this.backButton = document.createElement("button");
38     this.backButton.className = "status-bar-item";
39     this.backButton.id = "scripts-back";
40     this.backButton.title = WebInspector.UIString("Show the previous script resource.");
41     this.backButton.disabled = true;
42     this.backButton.appendChild(document.createElement("img"));
43     this.topStatusBar.appendChild(this.backButton);
44
45     this.forwardButton = document.createElement("button");
46     this.forwardButton.className = "status-bar-item";
47     this.forwardButton.id = "scripts-forward";
48     this.forwardButton.title = WebInspector.UIString("Show the next script resource.");
49     this.forwardButton.disabled = true;
50     this.forwardButton.appendChild(document.createElement("img"));
51     this.topStatusBar.appendChild(this.forwardButton);
52
53     this.filesSelectElement = document.createElement("select");
54     this.filesSelectElement.className = "status-bar-item";
55     this.filesSelectElement.id = "scripts-files";
56     this.filesSelectElement.addEventListener("change", this._changeVisibleFile.bind(this), false);
57     this.topStatusBar.appendChild(this.filesSelectElement);
58
59     this.functionsSelectElement = document.createElement("select");
60     this.functionsSelectElement.className = "status-bar-item";
61     this.functionsSelectElement.id = "scripts-functions";
62     this.topStatusBar.appendChild(this.functionsSelectElement);
63
64     this.sidebarButtonsElement = document.createElement("div");
65     this.sidebarButtonsElement.id = "scripts-sidebar-buttons";
66     this.topStatusBar.appendChild(this.sidebarButtonsElement);
67
68     this.pauseButton = document.createElement("button");
69     this.pauseButton.className = "status-bar-item";
70     this.pauseButton.id = "scripts-pause";
71     this.pauseButton.title = WebInspector.UIString("Pause script execution.");
72     this.pauseButton.disabled = true;
73     this.pauseButton.appendChild(document.createElement("img"));
74     this.pauseButton.addEventListener("click", this._togglePause.bind(this), false);
75     this.sidebarButtonsElement.appendChild(this.pauseButton);
76
77     this.stepOverButton = document.createElement("button");
78     this.stepOverButton.className = "status-bar-item";
79     this.stepOverButton.id = "scripts-step-over";
80     this.stepOverButton.title = WebInspector.UIString("Step over next function call.");
81     this.stepOverButton.disabled = true;
82     this.stepOverButton.addEventListener("click", this._stepOverClicked.bind(this), false);
83     this.stepOverButton.appendChild(document.createElement("img"));
84     this.sidebarButtonsElement.appendChild(this.stepOverButton);
85
86     this.stepIntoButton = document.createElement("button");
87     this.stepIntoButton.className = "status-bar-item";
88     this.stepIntoButton.id = "scripts-step-into";
89     this.stepIntoButton.title = WebInspector.UIString("Step into next function call.");
90     this.stepIntoButton.disabled = true;
91     this.stepIntoButton.addEventListener("click", this._stepIntoClicked.bind(this), false);
92     this.stepIntoButton.appendChild(document.createElement("img"));
93     this.sidebarButtonsElement.appendChild(this.stepIntoButton);
94
95     this.stepOutButton = document.createElement("button");
96     this.stepOutButton.className = "status-bar-item";
97     this.stepOutButton.id = "scripts-step-out";
98     this.stepOutButton.title = WebInspector.UIString("Step out of current function.");
99     this.stepOutButton.disabled = true;
100     this.stepOutButton.addEventListener("click", this._stepOutClicked.bind(this), false);
101     this.stepOutButton.appendChild(document.createElement("img"));
102     this.sidebarButtonsElement.appendChild(this.stepOutButton);
103
104     this.debuggerStatusElement = document.createElement("div");
105     this.debuggerStatusElement.id = "scripts-debugger-status";
106     this.sidebarButtonsElement.appendChild(this.debuggerStatusElement);
107
108     this.scriptResourceViews = document.createElement("div");
109     this.scriptResourceViews.id = "script-resource-views";
110
111     this.sidebarElement = document.createElement("div");
112     this.sidebarElement.id = "scripts-sidebar";
113
114     this.sidebarResizeElement = document.createElement("div");
115     this.sidebarResizeElement.className = "sidebar-resizer-vertical";
116     this.sidebarResizeElement.addEventListener("mousedown", this._startSidebarResizeDrag.bind(this), false);
117
118     this.sidebarResizeWidgetElement = document.createElement("div");
119     this.sidebarResizeWidgetElement.id = "scripts-sidebar-resizer-widget";
120     this.sidebarResizeWidgetElement.addEventListener("mousedown", this._startSidebarResizeDrag.bind(this), false);
121     this.topStatusBar.appendChild(this.sidebarResizeWidgetElement);
122
123     this.sidebarPanes = {};
124     this.sidebarPanes.callstack = new WebInspector.CallStackSidebarPane();
125     this.sidebarPanes.scopechain = new WebInspector.ScopeChainSidebarPane();
126     this.sidebarPanes.breakpoints = new WebInspector.BreakpointsSidebarPane();
127
128     for (var pane in this.sidebarPanes)
129         this.sidebarElement.appendChild(this.sidebarPanes[pane].element);
130
131     this.sidebarPanes.callstack.expanded = true;
132     this.sidebarPanes.callstack.addEventListener("call frame selected", this._callFrameSelected, this);
133
134     this.sidebarPanes.scopechain.expanded = true;
135
136     this.attachOverlayElement = document.createElement("div");
137     this.attachOverlayElement.id = "scripts-attach-overlay";
138
139     var headerElement = document.createElement("h1");
140     headerElement.textContent = WebInspector.UIString("Debugging scripts requires you to attach the debugger.");
141     this.attachOverlayElement.appendChild(headerElement);
142
143     var infoElement = document.createElement("span");
144     infoElement.textContent = WebInspector.UIString("Attaching will reload the inspected page.");
145     this.attachOverlayElement.appendChild(infoElement);
146
147     this.attachOverlayElement.appendChild(document.createElement("br"));
148     this.attachOverlayElement.appendChild(document.createElement("br"));
149
150     var attachButton = document.createElement("button");
151     attachButton.textContent = WebInspector.UIString("Attach Debugger");
152     attachButton.addEventListener("click", this._toggleDebugging.bind(this), false);
153     this.attachOverlayElement.appendChild(attachButton);
154
155     this.element.appendChild(this.attachOverlayElement);
156     this.element.appendChild(this.scriptResourceViews);
157     this.element.appendChild(this.sidebarElement);
158     this.element.appendChild(this.sidebarResizeElement);
159
160     this.debuggingButton = document.createElement("button");
161     this.debuggingButton.id = "scripts-debugging-status-bar-item";
162     this.debuggingButton.className = "status-bar-item";
163     this.debuggingButton.addEventListener("click", this._toggleDebugging.bind(this), false);
164
165     this._breakpointsURLMap = {};
166
167     this.reset();
168 }
169
170 WebInspector.ScriptsPanel.prototype = {
171     toolbarItemClass: "scripts",
172
173     get toolbarItemLabel()
174     {
175         return WebInspector.UIString("Scripts");
176     },
177
178     get statusBarItems()
179     {
180         return [this.debuggingButton];
181     },
182
183     show: function()
184     {
185         WebInspector.Panel.prototype.show.call(this);
186         this.sidebarResizeElement.style.right = (this.sidebarElement.offsetWidth - 3) + "px";
187
188         if (this.visibleView) {
189             if (this.visibleView instanceof WebInspector.ResourceView)
190                 this.visibleView.headersVisible = false;
191             this.visibleView.show(this.scriptResourceViews);
192         }
193     },
194
195     addScript: function(sourceID, sourceURL, source, startingLine, errorLine, errorMessage)
196     {
197         var script = new WebInspector.Script(sourceID, sourceURL, source, startingLine, errorLine, errorMessage);
198
199         if (sourceURL in WebInspector.resourceURLMap) {
200             var resource = WebInspector.resourceURLMap[sourceURL];
201             resource.addScript(script);
202         }
203
204         if (sourceURL in this._breakpointsURLMap && sourceID) {
205             var breakpoints = this._breakpointsURLMap[sourceURL];
206             var breakpointsLength = breakpoints.length;
207             for (var i = 0; i < breakpointsLength; ++i) {
208                 var breakpoint = breakpoints[i];
209                 if (startingLine <= breakpoint.line)
210                     breakpoint.sourceID = sourceID;
211             }
212
213             InspectorController.addBreakpoint(breakpoint.sourceID, breakpoint.line);
214         }
215
216         if (sourceID)
217             this._sourceIDMap[sourceID] = (resource || script);
218
219         this._addScriptToFilesMenu(script);
220     },
221
222     addBreakpoint: function(breakpoint)
223     {
224         this.sidebarPanes.breakpoints.addBreakpoint(breakpoint);
225
226         var sourceFrame;
227         if (breakpoint.url) {
228             if (!(breakpoint.url in this._breakpointsURLMap))
229                 this._breakpointsURLMap[breakpoint.url] = [];
230             this._breakpointsURLMap[breakpoint.url].unshift(breakpoint);
231
232             if (breakpoint.url in WebInspector.resourceURLMap) {
233                 var resource = WebInspector.resourceURLMap[breakpoint.url];
234                 sourceFrame = this._sourceFrameForScriptOrResource(resource);
235             }
236         }
237
238         if (breakpoint.sourceID && !sourceFrame) {
239             var object = this._sourceIDMap[breakpoint.sourceID]
240             sourceFrame = this._sourceFrameForScriptOrResource(object);
241         }
242
243         if (sourceFrame)
244             sourceFrame.addBreakpoint(breakpoint);
245     },
246
247     removeBreakpoint: function(breakpoint)
248     {
249         this.sidebarPanes.breakpoints.removeBreakpoint(breakpoint);
250
251         var sourceFrame;
252         if (breakpoint.url && breakpoint.url in this._breakpointsURLMap) {
253             var breakpoints = this._breakpointsURLMap[breakpoint.url];
254             breakpoints.remove(breakpoint);
255             if (!breakpoints.length)
256                 delete this._breakpointsURLMap[breakpoint.url];
257
258             if (breakpoint.url in WebInspector.resourceURLMap) {
259                 var resource = WebInspector.resourceURLMap[breakpoint.url];
260                 sourceFrame = this._sourceFrameForScriptOrResource(resource);
261             }
262         }
263
264         if (breakpoint.sourceID && !sourceFrame) {
265             var object = this._sourceIDMap[breakpoint.sourceID]
266             sourceFrame = this._sourceFrameForScriptOrResource(object);
267         }
268
269         if (sourceFrame)
270             sourceFrame.removeBreakpoint(breakpoint);
271     },
272
273     debuggerPaused: function()
274     {
275         this._paused = true;
276         this._waitingToPause = false;
277         this._stepping = false;
278
279         this._updateDebuggerButtons();
280
281         var callStackPane = this.sidebarPanes.callstack;
282         var currentFrame = InspectorController.currentCallFrame();
283         callStackPane.update(currentFrame);
284         callStackPane.selectedCallFrame = currentFrame;
285     },
286
287     reset: function()
288     {
289         this.visibleView = null;
290
291         if (!InspectorController.debuggerAttached()) {
292             this._paused = false;
293             this._waitingToPause = false;
294             this._stepping = false;
295         }
296
297         this._clearInterface();
298
299         this.filesSelectElement.removeChildren();
300         this.functionsSelectElement.removeChildren();
301         this.scriptResourceViews.removeChildren();
302
303         if (this._sourceIDMap) {
304             for (var sourceID in this._sourceIDMap) {
305                 var object = this._sourceIDMap[sourceID];
306                 if (object instanceof WebInspector.Resource)
307                     object.removeAllScripts();
308             }
309         }
310
311         this._sourceIDMap = {};
312     },
313
314     get visibleView()
315     {
316         return this._visibleView;
317     },
318
319     set visibleView(x)
320     {
321         if (this._visibleView === x)
322             return;
323
324         if (this._visibleView)
325             this._visibleView.hide();
326
327         this._visibleView = x;
328
329         if (x)
330             x.show(this.scriptResourceViews);
331     },
332
333     showScript: function(script, line)
334     {
335         this._showScriptOrResource(script, line);
336     },
337
338     showResource: function(resource, line)
339     {
340         this._showScriptOrResource(resource, line);
341     },
342
343     scriptViewForScript: function(script)
344     {
345         if (!script)
346             return null;
347         if (!script._scriptView)
348             script._scriptView = new WebInspector.ScriptView(script);
349         return script._scriptView;
350     },
351
352     sourceFrameForScript: function(script)
353     {
354         var view = this.scriptViewForScript(script);
355         if (!view)
356             return null;
357
358         // Setting up the source frame requires that we be attached.
359         if (!this.element.parentNode)
360             this.attach();
361
362         view.setupSourceFrameIfNeeded();
363         return view.sourceFrame;
364     },
365
366     _sourceFrameForScriptOrResource: function(scriptOrResource)
367     {
368         if (scriptOrResource instanceof WebInspector.Resource)
369             return WebInspector.panels.resources.sourceFrameForResource(scriptOrResource);
370         if (scriptOrResource instanceof WebInspector.Script)
371             return this.sourceFrameForScript(scriptOrResource);
372     },
373
374     _showScriptOrResource: function(scriptOrResource, line)
375     {
376         if (!scriptOrResource)
377             return;
378
379         var view;
380         if (scriptOrResource instanceof WebInspector.Resource) {
381             view = WebInspector.panels.resources.resourceViewForResource(scriptOrResource);
382             view.headersVisible = false;
383
384             if (scriptOrResource.url in this._breakpointsURLMap) {
385                 var sourceFrame = this._sourceFrameForScriptOrResource(scriptOrResource);
386                 if (sourceFrame && !sourceFrame.breakpoints.length) {
387                     var breakpoints = this._breakpointsURLMap[scriptOrResource.url];
388                     var breakpointsLength = breakpoints.length;
389                     for (var i = 0; i < breakpointsLength; ++i)
390                         sourceFrame.addBreakpoint(breakpoints[i]);
391                 }
392             }
393         } else if (scriptOrResource instanceof WebInspector.Script)
394             view = this.scriptViewForScript(scriptOrResource);
395
396         if (!view)
397             return;
398
399         this.visibleView = view;
400
401         if (line && view.revealLine)
402             view.revealLine(line);
403
404         var select = this.filesSelectElement;
405         var options = select.options;
406         for (var i = 0; i < options.length; ++i) {
407             if (options[i].representedObject === scriptOrResource)
408                 break;
409         }
410
411         select.selectedIndex = i;
412     },
413
414     _addScriptToFilesMenu: function(script)
415     {
416         var select = this.filesSelectElement;
417         var options = select.options;
418         for (var i = 0; i < options.length; ++i) {
419             var option = options[i];
420             if (option.representedObject === (script.resource || script))
421                 return;
422         }
423
424         // FIXME: Append in some meaningful order.
425         var option = document.createElement("option");
426         option.representedObject = (script.resource || script);
427         option.text = (script.sourceURL ? script.sourceURL.trimURL(WebInspector.mainResource ? WebInspector.mainResource.domain : "") : "(eval script)");
428         select.appendChild(option);
429
430         // Call _showScriptOrResource if the option we just appended ended up being selected.
431         // This will happen for the first item added to the menu.
432         if (options[select.selectedIndex] === option)
433             this._showScriptOrResource(option.representedObject);
434     },
435
436     _removeScriptFromFilesMenu: function(script)
437     {
438         // This function assumes it is called before removeScript is called on the resource.
439         if (script.resource && script.resource.scripts.length > 1)
440             return; // The resource has more than one script, so keep the resource in the menu.
441
442         var select = this.filesSelectElement;
443         var options = select.options;
444         for (var i = 0; i < options.length; ++i) {
445             if (option.representedObject !== script.resource && option.representedObject !== script)
446                 continue;
447
448             if (select.selectedIndex === i) {
449                 // Pick the next selectedIndex. If we're at the end of the list, loop back to beginning.
450                 var nextSelectedIndex = ((select.selectedIndex + 1) >= select.options.length ? 0 : select.selectedIndex);
451             }
452
453             // Remove the option from the select
454             select.options[select.selectedIndex] = null;
455
456             if (nextSelectedIndex)
457                 select.selectedIndex = nextSelectedIndex;
458         }
459     },
460
461     _clearCurrentExecutionLine: function()
462     {
463         if (this._executionSourceFrame)
464             this._executionSourceFrame.executionLine = 0;
465         delete this._executionSourceFrame;
466     },
467
468     _callFrameSelected: function()
469     {
470         this._clearCurrentExecutionLine();
471
472         var callStackPane = this.sidebarPanes.callstack;
473         var currentFrame = callStackPane.selectedCallFrame;
474         if (!currentFrame)
475             return;
476
477         this.sidebarPanes.scopechain.update(currentFrame);
478
479         var scriptOrResource = this._sourceIDMap[currentFrame.sourceIdentifier];
480         this._showScriptOrResource(scriptOrResource, currentFrame.line);
481
482         this._executionSourceFrame = this._sourceFrameForScriptOrResource(scriptOrResource);
483         if (this._executionSourceFrame)
484             this._executionSourceFrame.executionLine = currentFrame.line;
485     },
486
487     _changeVisibleFile: function(event)
488     {
489         var select = this.filesSelectElement;
490         this._showScriptOrResource(select.options[select.selectedIndex].representedObject);
491     },
492
493     _startSidebarResizeDrag: function(event)
494     {
495         WebInspector.elementDragStart(this.sidebarElement, this._sidebarResizeDrag.bind(this), this._endSidebarResizeDrag.bind(this), event, "col-resize");
496
497         if (event.target === this.sidebarResizeWidgetElement)
498             this._dragOffset = (event.target.offsetWidth - (event.pageX - event.target.totalOffsetLeft));
499         else
500             this._dragOffset = 0;
501     },
502
503     _endSidebarResizeDrag: function(event)
504     {
505         WebInspector.elementDragEnd(event);
506
507         delete this._dragOffset;
508     },
509
510     _sidebarResizeDrag: function(event)
511     {
512         var x = event.pageX + this._dragOffset;
513         var newWidth = Number.constrain(window.innerWidth - x, Preferences.minScriptsSidebarWidth, window.innerWidth * 0.66);
514
515         this.sidebarElement.style.width = newWidth + "px";
516         this.sidebarButtonsElement.style.width = newWidth + "px";
517         this.scriptResourceViews.style.right = newWidth + "px";
518         this.sidebarResizeWidgetElement.style.right = newWidth + "px";
519         this.sidebarResizeElement.style.right = (newWidth - 3) + "px";
520
521         event.preventDefault();
522     },
523
524     _updateDebuggerButtons: function()
525     {
526         if (InspectorController.debuggerAttached()) {
527             this.debuggingButton.title = WebInspector.UIString("Stop debugging.");
528             this.debuggingButton.addStyleClass("toggled-on");
529             this.pauseButton.disabled = false;
530         } else {
531             this.debuggingButton.title = WebInspector.UIString("Start debugging and reload inspected page.");
532             this.debuggingButton.removeStyleClass("toggled-on");
533             this.pauseButton.disabled = true;
534         }
535
536         if (this._paused) {
537             this.pauseButton.addStyleClass("paused");
538
539             this.pauseButton.disabled = false;
540             this.stepOverButton.disabled = false;
541             this.stepIntoButton.disabled = false;
542             this.stepOutButton.disabled = false;
543
544             this.debuggerStatusElement.textContent = WebInspector.UIString("Paused");
545         } else {
546             this.pauseButton.removeStyleClass("paused");
547
548             this.pauseButton.disabled = this._waitingToPause;
549             this.stepOverButton.disabled = true;
550             this.stepIntoButton.disabled = true;
551             this.stepOutButton.disabled = true;
552
553             if (this._waitingToPause)
554                 this.debuggerStatusElement.textContent = WebInspector.UIString("Pausing");
555             else if (this._stepping)
556                 this.debuggerStatusElement.textContent = WebInspector.UIString("Stepping");
557             else
558                 this.debuggerStatusElement.textContent = "";
559         }
560     },
561
562     _clearInterface: function()
563     {
564         this.sidebarPanes.callstack.update(null);
565         this.sidebarPanes.scopechain.update(null);
566
567         this._clearCurrentExecutionLine();
568         this._updateDebuggerButtons();
569     },
570
571     _toggleDebugging: function()
572     {
573         this._paused = false;
574         this._waitingToPause = false;
575         this._stepping = false;
576
577         this._clearInterface();
578
579         if (InspectorController.debuggerAttached()) {
580             this.element.appendChild(this.attachOverlayElement);
581             InspectorController.stopDebugging();
582         } else {
583             this.attachOverlayElement.parentNode.removeChild(this.attachOverlayElement);
584             InspectorController.startDebuggingAndReloadInspectedPage();
585         }
586     },
587
588     _togglePause: function()
589     {
590         if (this._paused) {
591             this._paused = false;
592             this._waitingToPause = false;
593             InspectorController.resumeDebugger();
594         } else {
595             this._stepping = false;
596             this._waitingToPause = true;
597             InspectorController.pauseInDebugger();
598         }
599
600         this._clearInterface();
601     },
602
603     _stepOverClicked: function()
604     {
605         this._paused = false;
606         this._stepping = true;
607
608         this._clearInterface();
609
610         InspectorController.stepOverStatementInDebugger();
611     },
612
613     _stepIntoClicked: function()
614     {
615         this._paused = false;
616         this._stepping = true;
617
618         this._clearInterface();
619
620         InspectorController.stepIntoStatementInDebugger();
621     },
622
623     _stepOutClicked: function()
624     {
625         this._paused = false;
626         this._stepping = true;
627
628         this._clearInterface();
629
630         InspectorController.stepOutOfFunctionInDebugger();
631     }
632 }
633
634 WebInspector.ScriptsPanel.prototype.__proto__ = WebInspector.Panel.prototype;