2011-03-12 Ilya Tikhonovsky <loislo@chromium.org>
[WebKit-https.git] / Source / WebCore / inspector / front-end / BreakpointManager.js
1 /*
2  * Copyright (C) 2010 Google 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 are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google Inc. nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30
31 WebInspector.BreakpointManager = function()
32 {
33     this._stickyBreakpoints = {};
34     var breakpoints = WebInspector.settings.findSettingForAllProjects("nativeBreakpoints");
35     for (var projectId in breakpoints)
36         this._stickyBreakpoints[projectId] = this._validateBreakpoints(breakpoints[projectId]);
37
38     this._breakpoints = {};
39     this._domBreakpointsRestored = false;
40
41     WebInspector.settings.addEventListener(WebInspector.Settings.Events.ProjectChanged, this._projectChanged, this);
42     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerPaused, this._debuggerPaused, this);
43     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerResumed, this._debuggerResumed, this);
44 }
45
46 WebInspector.BreakpointManager.BreakpointTypes = {
47     DOM: "DOM",
48     EventListener: "EventListener",
49     XHR: "XHR"
50 }
51
52 WebInspector.BreakpointManager.Events = {
53     DOMBreakpointAdded: "dom-breakpoint-added",
54     EventListenerBreakpointAdded: "event-listener-breakpoint-added",
55     XHRBreakpointAdded: "xhr-breakpoint-added",
56     ProjectChanged: "project-changed"
57 }
58
59 WebInspector.BreakpointManager.prototype = {
60     createDOMBreakpoint: function(nodeId, type)
61     {
62         this._createDOMBreakpoint(nodeId, type, true, false);
63     },
64
65     _createDOMBreakpoint: function(nodeId, type, enabled, restored)
66     {
67         var node = WebInspector.domAgent.nodeForId(nodeId);
68         if (!node)
69             return;
70
71         var breakpointId = this._createDOMBreakpointId(nodeId, type);
72         if (breakpointId in this._breakpoints)
73             return;
74
75         var breakpoint = new WebInspector.DOMBreakpoint(node, type);
76         this._setBreakpoint(breakpointId, breakpoint, enabled, restored);
77         if (enabled && restored)
78             breakpoint._enable();
79
80         breakpoint.view = new WebInspector.DOMBreakpointView(this, breakpointId, enabled, node, type);
81         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.DOMBreakpointAdded, breakpoint.view);
82     },
83
84     createEventListenerBreakpoint: function(eventName)
85     {
86         this._createEventListenerBreakpoint(eventName, true, false);
87     },
88
89     _createEventListenerBreakpoint: function(eventName, enabled, restored)
90     {
91         var breakpointId = this._createEventListenerBreakpointId(eventName);
92         if (breakpointId in this._breakpoints)
93             return;
94
95         var breakpoint = new WebInspector.EventListenerBreakpoint(eventName);
96         this._setBreakpoint(breakpointId, breakpoint, enabled, restored);
97
98         breakpoint.view = new WebInspector.EventListenerBreakpointView(this, breakpointId, enabled, eventName);
99         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.EventListenerBreakpointAdded, breakpoint.view);
100     },
101
102     createXHRBreakpoint: function(url)
103     {
104         this._createXHRBreakpoint(url, true, false);
105     },
106
107     _createXHRBreakpoint: function(url, enabled, restored)
108     {
109         var breakpointId = this._createXHRBreakpointId(url);
110         if (breakpointId in this._breakpoints)
111             return;
112
113         var breakpoint = new WebInspector.XHRBreakpoint(url);
114         this._setBreakpoint(breakpointId, breakpoint, enabled, restored);
115
116         breakpoint.view = new WebInspector.XHRBreakpointView(this, breakpointId, enabled, url);
117         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.XHRBreakpointAdded, breakpoint.view);
118     },
119
120     _setBreakpoint: function(breakpointId, breakpoint, enabled, restored)
121     {
122         this._breakpoints[breakpointId] = breakpoint;
123         breakpoint.enabled = enabled;
124         if (restored)
125             return;
126         if (enabled)
127             breakpoint._enable();
128         this._saveBreakpoints();
129     },
130
131     _setBreakpointEnabled: function(breakpointId, enabled)
132     {
133         var breakpoint = this._breakpoints[breakpointId];
134         if (breakpoint.enabled === enabled)
135             return;
136         if (enabled)
137             breakpoint._enable();
138         else
139             breakpoint._disable();
140         breakpoint.enabled = enabled;
141         this._saveBreakpoints();
142     },
143
144     _removeBreakpoint: function(breakpointId)
145     {
146         var breakpoint = this._breakpoints[breakpointId];
147         if (breakpoint.enabled)
148             breakpoint._disable();
149         delete this._breakpoints[breakpointId];
150         this._saveBreakpoints();
151     },
152
153     breakpointViewForEventData: function(eventData)
154     {
155         var breakpointId;
156         if (eventData.breakpointType === WebInspector.BreakpointManager.BreakpointTypes.DOM)
157             breakpointId = this._createDOMBreakpointId(eventData.nodeId, eventData.type);
158         else if (eventData.breakpointType === WebInspector.BreakpointManager.BreakpointTypes.EventListener)
159             breakpointId = this._createEventListenerBreakpointId(eventData.eventName);
160         else if (eventData.breakpointType === WebInspector.BreakpointManager.BreakpointTypes.XHR)
161             breakpointId = this._createXHRBreakpointId(eventData.breakpointURL);
162         else
163             return;
164
165         var breakpoint = this._breakpoints[breakpointId];
166         if (breakpoint)
167             return breakpoint.view;
168     },
169
170     _debuggerPaused: function(event)
171     {
172         var eventType = event.data.eventType;
173         var eventData = event.data.eventData;
174
175         if (eventType !== WebInspector.DebuggerEventTypes.NativeBreakpoint)
176             return;
177
178         var breakpointView = this.breakpointViewForEventData(eventData);
179         if (!breakpointView)
180             return;
181
182         breakpointView.hit = true;
183         this._lastHitBreakpointView = breakpointView;
184     },
185
186     _debuggerResumed: function(event)
187     {
188         if (!this._lastHitBreakpointView)
189             return;
190         this._lastHitBreakpointView.hit = false;
191         delete this._lastHitBreakpointView;
192     },
193
194     _projectChanged: function(event)
195     {
196         this._breakpoints = {};
197         this._domBreakpointsRestored = false;
198         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.ProjectChanged);
199
200         var breakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
201         for (var i = 0; i < breakpoints.length; ++i) {
202             var breakpoint = breakpoints[i];
203             if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.EventListener)
204                 this._createEventListenerBreakpoint(breakpoint.condition.eventName, breakpoint.enabled, true);
205             else if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.XHR)
206                 this._createXHRBreakpoint(breakpoint.condition.url, breakpoint.enabled, true);
207         }
208
209         if (!this._breakpointsPushedToFrontend) {
210             BrowserDebuggerAgent.setAllBrowserBreakpoints(this._stickyBreakpoints);
211             this._breakpointsPushedToFrontend = true;
212         }
213     },
214
215     restoreDOMBreakpoints: function()
216     {
217         function didPushNodeByPathToFrontend(path, nodeId)
218         {
219             if (!nodeId)
220                 return;
221
222             pathToNodeId[path] = nodeId;
223             pendingCalls -= 1;
224             if (pendingCalls)
225                 return;
226             for (var i = 0; i < breakpoints.length; ++i) {
227                 var breakpoint = breakpoints[i];
228                 if (breakpoint.type !== WebInspector.BreakpointManager.BreakpointTypes.DOM)
229                     continue;
230                 var nodeId = pathToNodeId[breakpoint.condition.path];
231                 if (nodeId)
232                     this._createDOMBreakpoint(nodeId, breakpoint.condition.type, breakpoint.enabled, true);
233             }
234             this._domBreakpointsRestored = true;
235             this._saveBreakpoints();
236         }
237
238         var breakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
239         var pathToNodeId = {};
240         var pendingCalls = 0;
241         for (var i = 0; i < breakpoints.length; ++i) {
242             if (breakpoints[i].type !== WebInspector.BreakpointManager.BreakpointTypes.DOM)
243                 continue;
244             var path = breakpoints[i].condition.path;
245             if (path in pathToNodeId)
246                 continue;
247             pathToNodeId[path] = 0;
248             pendingCalls += 1;
249             WebInspector.domAgent.pushNodeByPathToFrontend(path, didPushNodeByPathToFrontend.bind(this, path));
250         }
251         if (!pendingCalls)
252             this._domBreakpointsRestored = true;
253     },
254
255     _saveBreakpoints: function()
256     {
257         var breakpoints = [];
258         for (var breakpointId in this._breakpoints) {
259             var breakpoint = this._breakpoints[breakpointId];
260             var persistentBreakpoint = breakpoint._serializeToJSON();
261             persistentBreakpoint.enabled = breakpoint.enabled;
262             breakpoints.push(persistentBreakpoint);
263         }
264         if (!this._domBreakpointsRestored) {
265             var stickyBreakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
266             for (var i = 0; i < stickyBreakpoints.length; ++i) {
267                 if (stickyBreakpoints[i].type === WebInspector.BreakpointManager.BreakpointTypes.DOM)
268                     breakpoints.push(stickyBreakpoints[i]);
269             }
270         }
271         WebInspector.settings.nativeBreakpoints = breakpoints;
272
273         this._stickyBreakpoints[WebInspector.settings.projectId] = breakpoints;
274         BrowserDebuggerAgent.setAllBrowserBreakpoints(this._stickyBreakpoints);
275     },
276
277     _validateBreakpoints: function(persistentBreakpoints)
278     {
279         var breakpoints = [];
280         var breakpointsSet = {};
281         for (var i = 0; i < persistentBreakpoints.length; ++i) {
282             var breakpoint = persistentBreakpoints[i];
283             if (!("type" in breakpoint && "enabled" in breakpoint && "condition" in breakpoint))
284                 continue;
285             var id = breakpoint.type + ":";
286             var condition = breakpoint.condition;
287             if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.DOM) {
288                 if (typeof condition.path !== "string" || typeof condition.type !== "number")
289                     continue;
290                 id += condition.path + ":" + condition.type;
291             } else if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.EventListener) {
292                 if (typeof condition.eventName !== "string")
293                     continue;
294                 id += condition.eventName;
295             } else if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.XHR) {
296                 if (typeof condition.url !== "string")
297                     continue;
298                 id += condition.url;
299             } else
300                 continue;
301             if (id in breakpointsSet)
302                 continue;
303             breakpointsSet[id] = true;
304             breakpoints.push(breakpoint);
305         }
306         return breakpoints;
307     },
308
309     _createDOMBreakpointId: function(nodeId, type)
310     {
311         return "dom:" + nodeId + ":" + type;
312     },
313
314     _createEventListenerBreakpointId: function(eventName)
315     {
316         return "eventListner:" + eventName;
317     },
318
319     _createXHRBreakpointId: function(url)
320     {
321         return "xhr:" + url;
322     }
323 }
324
325 WebInspector.BreakpointManager.prototype.__proto__ = WebInspector.Object.prototype;
326
327 WebInspector.DOMBreakpoint = function(node, type)
328 {
329     this._nodeId = node.id;
330     this._path = node.path();
331     this._type = type;
332 }
333
334 WebInspector.DOMBreakpoint.prototype = {
335     _enable: function()
336     {
337         BrowserDebuggerAgent.setDOMBreakpoint(this._nodeId, this._type);
338     },
339
340     _disable: function()
341     {
342         BrowserDebuggerAgent.removeDOMBreakpoint(this._nodeId, this._type);
343     },
344
345     _serializeToJSON: function()
346     {
347         var type = WebInspector.BreakpointManager.BreakpointTypes.DOM;
348         return { type: type, condition: { path: this._path, type: this._type } };
349     }
350 }
351
352 WebInspector.EventListenerBreakpoint = function(eventName)
353 {
354     this._eventName = eventName;
355 }
356
357 WebInspector.EventListenerBreakpoint.prototype = {
358     _enable: function()
359     {
360         BrowserDebuggerAgent.setEventListenerBreakpoint(this._eventName);
361     },
362
363     _disable: function()
364     {
365         BrowserDebuggerAgent.removeEventListenerBreakpoint(this._eventName);
366     },
367
368     _serializeToJSON: function()
369     {
370         var type = WebInspector.BreakpointManager.BreakpointTypes.EventListener;
371         return { type: type, condition: { eventName: this._eventName } };
372     }
373 }
374
375 WebInspector.XHRBreakpoint = function(url)
376 {
377     this._url = url;
378 }
379
380 WebInspector.XHRBreakpoint.prototype = {
381     _enable: function()
382     {
383         BrowserDebuggerAgent.setXHRBreakpoint(this._url);
384     },
385
386     _disable: function()
387     {
388         BrowserDebuggerAgent.removeXHRBreakpoint(this._url);
389     },
390
391     _serializeToJSON: function()
392     {
393         var type = WebInspector.BreakpointManager.BreakpointTypes.XHR;
394         return { type: type, condition: { url: this._url } };
395     }
396 }
397
398
399
400 WebInspector.NativeBreakpointView = function(manager, id, enabled)
401 {
402     this._manager = manager;
403     this._id = id;
404     this._enabled = enabled;
405     this._hit = false;
406 }
407
408 WebInspector.NativeBreakpointView.prototype = {
409     get enabled()
410     {
411         return this._enabled;
412     },
413
414     set enabled(enabled)
415     {
416         this._manager._setBreakpointEnabled(this._id, enabled);
417         this._enabled = enabled;
418         this.dispatchEventToListeners("enable-changed");
419     },
420
421     get hit()
422     {
423         return this._hit;
424     },
425
426     set hit(hit)
427     {
428         this._hit = hit;
429         this.dispatchEventToListeners("hit-state-changed");
430     },
431
432     remove: function()
433     {
434         this._manager._removeBreakpoint(this._id);
435         this._onRemove();
436         this.dispatchEventToListeners("removed");
437     },
438
439     _compare: function(x, y)
440     {
441         if (x !== y)
442             return x < y ? -1 : 1;
443         return 0;
444     },
445
446     _onRemove: function()
447     {
448     }
449 }
450
451 WebInspector.NativeBreakpointView.prototype.__proto__ = WebInspector.Object.prototype;
452
453 WebInspector.DOMBreakpointView = function(manager, id, enabled, node, type)
454 {
455     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
456     this._node = node;
457     this._nodeId = node.id;
458     this._type = type;
459     node.breakpoints[this._type] = this;
460 }
461
462 WebInspector.DOMBreakpointView.prototype = {
463     compareTo: function(other)
464     {
465         return this._compare(this._type, other._type);
466     },
467
468     populateLabelElement: function(element)
469     {
470         // FIXME: this should belong to the view, not the manager.
471         var linkifiedNode = WebInspector.panels.elements.linkifyNodeById(this._nodeId);
472         linkifiedNode.addStyleClass("monospace");
473         element.appendChild(linkifiedNode);
474         var description = document.createElement("div");
475         description.className = "source-text";
476         description.textContent = WebInspector.domBreakpointTypeLabel(this._type);
477         element.appendChild(description);
478     },
479
480     populateStatusMessageElement: function(element, eventData)
481     {
482         if (this._type === WebInspector.DOMBreakpointTypes.SubtreeModified) {
483             var targetNodeObject = WebInspector.RemoteObject.fromPayload(eventData.targetNode);
484             targetNodeObject.pushNodeToFrontend(decorateNode.bind(this));
485             function decorateNode(targetNodeId)
486             {
487                 if (!targetNodeId)
488                     return;
489
490                 targetNodeObject.release();
491                 var targetNode = WebInspector.panels.elements.linkifyNodeById(targetNodeId);
492                 if (eventData.insertion) {
493                     if (targetNodeId !== this._nodeId)
494                         this._format(element, "Paused on a \"%s\" breakpoint set on %s, because a new child was added to its descendant %s.", targetNode);
495                     else
496                         this._format(element, "Paused on a \"%s\" breakpoint set on %s, because a new child was added to that node.");
497                 } else
498                     this._format(element, "Paused on a \"%s\" breakpoint set on %s, because its descendant %s was removed.", targetNode);
499             }
500         } else
501             this._format(element, "Paused on a \"%s\" breakpoint set on %s.");
502     },
503
504     _format: function(element, message, extraSubstitution)
505     {
506         var substitutions = [WebInspector.domBreakpointTypeLabel(this._type), WebInspector.panels.elements.linkifyNodeById(this._nodeId)];
507         if (extraSubstitution)
508             substitutions.push(extraSubstitution);
509
510         var formatters = {
511             s: function(substitution)
512             {
513                 return substitution;
514             }
515         };
516         function append(a, b)
517         {
518             if (typeof b === "string")
519                 b = document.createTextNode(b);
520             element.appendChild(b);
521         }
522         WebInspector.formatLocalized(message, substitutions, formatters, "", append);
523     },
524
525     _onRemove: function()
526     {
527         delete this._node.breakpoints[this._type];
528     }
529 }
530
531 WebInspector.DOMBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
532
533 WebInspector.EventListenerBreakpointView = function(manager, id, enabled, eventName)
534 {
535     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
536     this._eventName = eventName;
537 }
538
539 WebInspector.EventListenerBreakpointView.eventNameForUI = function(eventName)
540 {
541     if (!WebInspector.EventListenerBreakpointView._eventNamesForUI) {
542         WebInspector.EventListenerBreakpointView._eventNamesForUI = {
543             "instrumentation:setTimer": WebInspector.UIString("Set Timer"),
544             "instrumentation:clearTimer": WebInspector.UIString("Clear Timer"),
545             "instrumentation:timerFired": WebInspector.UIString("Timer Fired")
546         };
547     }
548     return WebInspector.EventListenerBreakpointView._eventNamesForUI[eventName] || eventName.substring(eventName.indexOf(":") + 1);
549 }
550
551 WebInspector.EventListenerBreakpointView.prototype = {
552     get eventName()
553     {
554         return this._eventName;
555     },
556
557     compareTo: function(other)
558     {
559         return this._compare(this._eventName, other._eventName);
560     },
561
562     populateLabelElement: function(element)
563     {
564         element.appendChild(document.createTextNode(this._uiEventName()));
565     },
566
567     populateStatusMessageElement: function(element, eventData)
568     {
569         var status = WebInspector.UIString("Paused on a \"%s\" Event Listener.", this._uiEventName());
570         element.appendChild(document.createTextNode(status));
571     },
572
573     _uiEventName: function()
574     {
575         return WebInspector.EventListenerBreakpointView.eventNameForUI(this._eventName);
576     }
577 }
578
579 WebInspector.EventListenerBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
580
581 WebInspector.XHRBreakpointView = function(manager, id, enabled, url)
582 {
583     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
584     this._url = url;
585 }
586
587 WebInspector.XHRBreakpointView.prototype = {
588     compareTo: function(other)
589     {
590         return this._compare(this._url, other._url);
591     },
592
593     populateEditElement: function(element)
594     {
595         element.textContent = this._url;
596     },
597
598     populateLabelElement: function(element)
599     {
600         var label;
601         if (!this._url.length)
602             label = WebInspector.UIString("Any XHR");
603         else
604             label = WebInspector.UIString("URL contains \"%s\"", this._url);
605         element.appendChild(document.createTextNode(label));
606         element.addStyleClass("cursor-auto");
607     },
608
609     populateStatusMessageElement: function(element)
610     {
611         var status = WebInspector.UIString("Paused on a XMLHttpRequest.");
612         element.appendChild(document.createTextNode(status));
613     }
614 }
615
616 WebInspector.XHRBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
617
618 WebInspector.DOMBreakpointTypes = {
619     SubtreeModified: 0,
620     AttributeModified: 1,
621     NodeRemoved: 2
622 };
623
624 WebInspector.domBreakpointTypeLabel = function(type)
625 {
626     if (!WebInspector._DOMBreakpointTypeLabels) {
627         WebInspector._DOMBreakpointTypeLabels = {};
628         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Subtree Modified");
629         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Attribute Modified");
630         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Node Removed");
631     }
632     return WebInspector._DOMBreakpointTypeLabels[type];
633 }
634
635 WebInspector.domBreakpointTypeContextMenuLabel = function(type)
636 {
637     if (!WebInspector._DOMBreakpointTypeContextMenuLabels) {
638         WebInspector._DOMBreakpointTypeContextMenuLabels = {};
639         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Break on Subtree Modifications");
640         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Break on Attributes Modifications");
641         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Break on Node Removal");
642     }
643     return WebInspector._DOMBreakpointTypeContextMenuLabels[type];
644 }