2010-12-22 Sheriff Bot <webkit.review.bot@gmail.com>
[WebKit-https.git] / 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     InspectorBackend.setStickyBreakpoints(this._stickyBreakpoints);
38
39     this._nativeBreakpoints = {};
40     this._domBreakpointsRestored = false;
41
42     WebInspector.settings.addEventListener(WebInspector.Settings.Events.ProjectChanged, this._projectChanged, this);
43     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerPaused, this._debuggerPaused, this);
44     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerResumed, this._debuggerResumed, this);
45 }
46
47 WebInspector.BreakpointManager.NativeBreakpointTypes = {
48     DOM: "DOM",
49     EventListener: "EventListener",
50     XHR: "XHR"
51 }
52
53 WebInspector.BreakpointManager.Events = {
54     DOMBreakpointAdded: "dom-breakpoint-added",
55     EventListenerBreakpointAdded: "event-listener-breakpoint-added",
56     XHRBreakpointAdded: "xhr-breakpoint-added",
57     ProjectChanged: "project-changed"
58 }
59
60 WebInspector.BreakpointManager.prototype = {
61     createDOMBreakpoint: function(nodeId, type)
62     {
63         this._createDOMBreakpoint(nodeId, type, true, false);
64     },
65
66     _createDOMBreakpoint: function(nodeId, type, enabled, restored)
67     {
68         var node = WebInspector.domAgent.nodeForId(nodeId);
69         if (!node)
70             return;
71
72         var breakpointId = this._createDOMBreakpointId(nodeId, type);
73         if (breakpointId in this._nativeBreakpoints)
74             return;
75
76         var breakpoint = new WebInspector.DOMBreakpoint(node, type);
77         this._setNativeBreakpoint(breakpointId, breakpoint, enabled, restored);
78         if (enabled && restored)
79             breakpoint._enable();
80
81         breakpoint.view = new WebInspector.DOMBreakpointView(this, breakpointId, enabled, node, type);
82         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.DOMBreakpointAdded, breakpoint.view);
83     },
84
85     createEventListenerBreakpoint: function(eventName)
86     {
87         this._createEventListenerBreakpoint(eventName, true, false);
88     },
89
90     _createEventListenerBreakpoint: function(eventName, enabled, restored)
91     {
92         var breakpointId = this._createEventListenerBreakpointId(eventName);
93         if (breakpointId in this._nativeBreakpoints)
94             return;
95
96         var breakpoint = new WebInspector.EventListenerBreakpoint(eventName);
97         this._setNativeBreakpoint(breakpointId, breakpoint, enabled, restored);
98
99         breakpoint.view = new WebInspector.EventListenerBreakpointView(this, breakpointId, enabled, eventName);
100         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.EventListenerBreakpointAdded, breakpoint.view);
101     },
102
103     createXHRBreakpoint: function(url)
104     {
105         this._createXHRBreakpoint(url, true, false);
106     },
107
108     _createXHRBreakpoint: function(url, enabled, restored)
109     {
110         var breakpointId = this._createXHRBreakpointId(url);
111         if (breakpointId in this._nativeBreakpoints)
112             return;
113
114         var breakpoint = new WebInspector.XHRBreakpoint(url);
115         this._setNativeBreakpoint(breakpointId, breakpoint, enabled, restored);
116
117         breakpoint.view = new WebInspector.XHRBreakpointView(this, breakpointId, enabled, url);
118         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.XHRBreakpointAdded, breakpoint.view);
119     },
120
121     _setNativeBreakpoint: function(breakpointId, breakpoint, enabled, restored)
122     {
123         this._nativeBreakpoints[breakpointId] = breakpoint;
124         breakpoint.enabled = enabled;
125         if (restored)
126             return;
127         if (enabled)
128             breakpoint._enable();
129         this._saveBreakpoints();
130     },
131
132     _setNativeBreakpointEnabled: function(breakpointId, enabled)
133     {
134         var breakpoint = this._nativeBreakpoints[breakpointId];
135         if (breakpoint.enabled === enabled)
136             return;
137         if (enabled)
138             breakpoint._enable();
139         else
140             breakpoint._disable();
141         breakpoint.enabled = enabled;
142         this._saveBreakpoints();
143     },
144
145     _removeNativeBreakpoint: function(breakpointId)
146     {
147         var breakpoint = this._nativeBreakpoints[breakpointId];
148         if (breakpoint.enabled)
149             breakpoint._disable();
150         delete this._nativeBreakpoints[breakpointId];
151         this._saveBreakpoints();
152     },
153
154     breakpointViewForEventData: function(eventData)
155     {
156         var breakpointId;
157         if (eventData.breakpointType === WebInspector.BreakpointManager.NativeBreakpointTypes.DOM)
158             breakpointId = this._createDOMBreakpointId(eventData.nodeId, eventData.type);
159         else if (eventData.breakpointType === WebInspector.BreakpointManager.NativeBreakpointTypes.EventListener)
160             breakpointId = this._createEventListenerBreakpointId(eventData.eventName);
161         else if (eventData.breakpointType === WebInspector.BreakpointManager.NativeBreakpointTypes.XHR)
162             breakpointId = this._createXHRBreakpointId(eventData.breakpointURL);
163         var breakpoint = this._nativeBreakpoints[breakpointId];
164         if (breakpoint)
165             return breakpoint.view;
166     },
167
168     _debuggerPaused: function(event)
169     {
170         var eventType = event.data.eventType;
171         var eventData = event.data.eventData;
172
173         if (eventType !== WebInspector.DebuggerEventTypes.NativeBreakpoint)
174             return;
175
176         var breakpointView = this.breakpointViewForEventData(eventData);
177         if (!breakpointView)
178             return;
179
180         breakpointView.hit = true;
181         this._lastHitBreakpointView = breakpointView;
182     },
183
184     _debuggerResumed: function(event)
185     {
186         if (!this._lastHitBreakpointView)
187             return;
188         this._lastHitBreakpointView.hit = false;
189         delete this._lastHitBreakpointView;
190     },
191
192     _projectChanged: function(event)
193     {
194         this._nativeBreakpoints = {};
195         this._domBreakpointsRestored = false;
196         this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.ProjectChanged);
197
198         var breakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
199         for (var i = 0; i < breakpoints.length; ++i) {
200             var breakpoint = breakpoints[i];
201             if (breakpoint.type === WebInspector.BreakpointManager.NativeBreakpointTypes.EventListener)
202                 this._createEventListenerBreakpoint(breakpoint.condition.eventName, breakpoint.enabled, true);
203             else if (breakpoint.type === WebInspector.BreakpointManager.NativeBreakpointTypes.XHR)
204                 this._createXHRBreakpoint(breakpoint.condition.url, breakpoint.enabled, true);
205         }
206     },
207
208     restoreDOMBreakpoints: function()
209     {
210         function didPushNodeByPathToFrontend(path, nodeId)
211         {
212             pathToNodeId[path] = nodeId;
213             pendingCalls -= 1;
214             if (pendingCalls)
215                 return;
216             for (var i = 0; i < breakpoints.length; ++i) {
217                 var breakpoint = breakpoints[i];
218                 if (breakpoint.type !== WebInspector.BreakpointManager.NativeBreakpointTypes.DOM)
219                     continue;
220                 var nodeId = pathToNodeId[breakpoint.condition.path];
221                 if (nodeId)
222                     this._createDOMBreakpoint(nodeId, breakpoint.condition.type, breakpoint.enabled, true);
223             }
224             this._domBreakpointsRestored = true;
225             this._saveBreakpoints();
226         }
227
228         var breakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
229         var pathToNodeId = {};
230         var pendingCalls = 0;
231         for (var i = 0; i < breakpoints.length; ++i) {
232             if (breakpoints[i].type !== WebInspector.BreakpointManager.NativeBreakpointTypes.DOM)
233                 continue;
234             var path = breakpoints[i].condition.path;
235             if (path in pathToNodeId)
236                 continue;
237             pathToNodeId[path] = 0;
238             pendingCalls += 1;
239             InspectorBackend.pushNodeByPathToFrontend(path, didPushNodeByPathToFrontend.bind(this, path));
240         }
241         if (!pendingCalls)
242             this._domBreakpointsRestored = true;
243     },
244
245     _saveBreakpoints: function()
246     {
247         var breakpoints = [];
248         for (var breakpointId in this._nativeBreakpoints) {
249             var breakpoint = this._nativeBreakpoints[breakpointId];
250             var persistentBreakpoint = breakpoint._serializeToJSON();
251             persistentBreakpoint.enabled = breakpoint.enabled;
252             breakpoints.push(persistentBreakpoint);
253         }
254         if (!this._domBreakpointsRestored) {
255             var stickyBreakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
256             for (var i = 0; i < stickyBreakpoints.length; ++i) {
257                 if (stickyBreakpoints[i].type === WebInspector.BreakpointManager.NativeBreakpointTypes.DOM)
258                     breakpoints.push(stickyBreakpoints[i]);
259             }
260         }
261         WebInspector.settings.nativeBreakpoints = breakpoints;
262
263         this._stickyBreakpoints[WebInspector.settings.projectId] = breakpoints;
264         InspectorBackend.setStickyBreakpoints(this._stickyBreakpoints);
265     },
266
267     _validateBreakpoints: function(persistentBreakpoints)
268     {
269         var breakpoints = [];
270         var breakpointsSet = {};
271         for (var i = 0; i < persistentBreakpoints.length; ++i) {
272             var breakpoint = persistentBreakpoints[i];
273             if (!("type" in breakpoint && "enabled" in breakpoint && "condition" in breakpoint))
274                 continue;
275             var id = breakpoint.type + ":";
276             var condition = breakpoint.condition;
277             if (breakpoint.type === WebInspector.BreakpointManager.NativeBreakpointTypes.DOM) {
278                 if (typeof condition.path !== "string" || typeof condition.type !== "number")
279                     continue;
280                 id += condition.path + ":" + condition.type;
281             } else if (breakpoint.type === WebInspector.BreakpointManager.NativeBreakpointTypes.EventListener) {
282                 if (typeof condition.eventName !== "string")
283                     continue;
284                 id += condition.eventName;
285             } else if (breakpoint.type === WebInspector.BreakpointManager.NativeBreakpointTypes.XHR) {
286                 if (typeof condition.url !== "string")
287                     continue;
288                 id += condition.url;
289             }
290             if (id in breakpointsSet)
291                 continue;
292             breakpointsSet[id] = true;
293             breakpoints.push(breakpoint);
294         }
295         return breakpoints;
296     },
297
298     _createDOMBreakpointId: function(nodeId, type)
299     {
300         return "dom:" + nodeId + ":" + type;
301     },
302
303     _createEventListenerBreakpointId: function(eventName)
304     {
305         return "eventListner:" + eventName;
306     },
307
308     _createXHRBreakpointId: function(url)
309     {
310         return "xhr:" + url;
311     }
312 }
313
314 WebInspector.BreakpointManager.prototype.__proto__ = WebInspector.Object.prototype;
315
316 WebInspector.DOMBreakpoint = function(node, type)
317 {
318     this._nodeId = node.id;
319     this._path = node.path();
320     this._type = type;
321 }
322
323 WebInspector.DOMBreakpoint.prototype = {
324     _enable: function()
325     {
326         InspectorBackend.setDOMBreakpoint(this._nodeId, this._type);
327     },
328
329     _disable: function()
330     {
331         InspectorBackend.removeDOMBreakpoint(this._nodeId, this._type);
332     },
333
334     _serializeToJSON: function()
335     {
336         var type = WebInspector.BreakpointManager.NativeBreakpointTypes.DOM;
337         return { type: type, condition: { path: this._path, type: this._type } };
338     }
339 }
340
341 WebInspector.EventListenerBreakpoint = function(eventName)
342 {
343     this._eventName = eventName;
344 }
345
346 WebInspector.EventListenerBreakpoint.prototype = {
347     _enable: function()
348     {
349         InspectorBackend.setEventListenerBreakpoint(this._eventName);
350     },
351
352     _disable: function()
353     {
354         InspectorBackend.removeEventListenerBreakpoint(this._eventName);
355     },
356
357     _serializeToJSON: function()
358     {
359         var type = WebInspector.BreakpointManager.NativeBreakpointTypes.EventListener;
360         return { type: type, condition: { eventName: this._eventName } };
361     }
362 }
363
364 WebInspector.XHRBreakpoint = function(url)
365 {
366     this._url = url;
367 }
368
369 WebInspector.XHRBreakpoint.prototype = {
370     _enable: function()
371     {
372         InspectorBackend.setXHRBreakpoint(this._url);
373     },
374
375     _disable: function()
376     {
377         InspectorBackend.removeXHRBreakpoint(this._url);
378     },
379
380     _serializeToJSON: function()
381     {
382         var type = WebInspector.BreakpointManager.NativeBreakpointTypes.XHR;
383         return { type: type, condition: { url: this._url } };
384     }
385 }
386
387
388
389 WebInspector.NativeBreakpointView = function(manager, id, enabled)
390 {
391     this._manager = manager;
392     this._id = id;
393     this._enabled = enabled;
394     this._hit = false;
395 }
396
397 WebInspector.NativeBreakpointView.prototype = {
398     get enabled()
399     {
400         return this._enabled;
401     },
402
403     set enabled(enabled)
404     {
405         this._manager._setNativeBreakpointEnabled(this._id, enabled);
406         this._enabled = enabled;
407         this.dispatchEventToListeners("enable-changed");
408     },
409
410     get hit()
411     {
412         return this._hit;
413     },
414
415     set hit(hit)
416     {
417         this._hit = hit;
418         this.dispatchEventToListeners("hit-state-changed");
419     },
420
421     remove: function()
422     {
423         this._manager._removeNativeBreakpoint(this._id);
424         this._onRemove();
425         this.dispatchEventToListeners("removed");
426     },
427
428     _compare: function(x, y)
429     {
430         if (x !== y)
431             return x < y ? -1 : 1;
432         return 0;
433     },
434
435     _onRemove: function()
436     {
437     }
438 }
439
440 WebInspector.NativeBreakpointView.prototype.__proto__ = WebInspector.Object.prototype;
441
442 WebInspector.DOMBreakpointView = function(manager, id, enabled, node, type)
443 {
444     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
445     this._node = node;
446     this._nodeId = node.id;
447     this._type = type;
448     node.breakpoints[this._type] = this;
449 }
450
451 WebInspector.DOMBreakpointView.prototype = {
452     compareTo: function(other)
453     {
454         return this._compare(this._type, other._type);
455     },
456
457     populateLabelElement: function(element)
458     {
459         // FIXME: this should belong to the view, not the manager.
460         var linkifiedNode = WebInspector.panels.elements.linkifyNodeById(this._nodeId);
461         linkifiedNode.addStyleClass("monospace");
462         element.appendChild(linkifiedNode);
463         var description = document.createElement("div");
464         description.className = "source-text";
465         description.textContent = WebInspector.domBreakpointTypeLabel(this._type);
466         element.appendChild(description);
467     },
468
469     populateStatusMessageElement: function(element, eventData)
470     {
471         var substitutions = [WebInspector.domBreakpointTypeLabel(this._type), WebInspector.panels.elements.linkifyNodeById(this._nodeId)];
472         var formatters = {
473             s: function(substitution)
474             {
475                 return substitution;
476             }
477         };
478         function append(a, b)
479         {
480             if (typeof b === "string")
481                 b = document.createTextNode(b);
482             element.appendChild(b);
483         }
484         if (this._type === WebInspector.DOMBreakpointTypes.SubtreeModified) {
485             var targetNode = WebInspector.panels.elements.linkifyNodeById(eventData.targetNodeId);
486             if (eventData.insertion) {
487                 if (eventData.targetNodeId !== this._nodeId)
488                     WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s, because a new child was added to its descendant %s.", substitutions.concat(targetNode), formatters, "", append);
489                 else
490                     WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s, because a new child was added to that node.", substitutions, formatters, "", append);
491             } else
492                 WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s, because its descendant %s was removed.", substitutions.concat(targetNode), formatters, "", append);
493         } else
494             WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s.", substitutions, formatters, "", append);
495     },
496
497     _onRemove: function()
498     {
499         delete this._node.breakpoints[this._type];
500     }
501 }
502
503 WebInspector.DOMBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
504
505 WebInspector.EventListenerBreakpointView = function(manager, id, enabled, eventName)
506 {
507     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
508     this._eventName = eventName;
509 }
510
511 WebInspector.EventListenerBreakpointView.eventNameForUI = function(eventName)
512 {
513     if (!WebInspector.EventListenerBreakpointView._eventNamesForUI) {
514         WebInspector.EventListenerBreakpointView._eventNamesForUI = {
515             "instrumentation:setTimer": WebInspector.UIString("Set Timer"),
516             "instrumentation:clearTimer": WebInspector.UIString("Clear Timer"),
517             "instrumentation:timerFired": WebInspector.UIString("Timer Fired")
518         };
519     }
520     return WebInspector.EventListenerBreakpointView._eventNamesForUI[eventName] || eventName.substring(eventName.indexOf(":") + 1);
521 }
522
523 WebInspector.EventListenerBreakpointView.prototype = {
524     get eventName()
525     {
526         return this._eventName;
527     },
528
529     compareTo: function(other)
530     {
531         return this._compare(this._eventName, other._eventName);
532     },
533
534     populateLabelElement: function(element)
535     {
536         element.appendChild(document.createTextNode(this._uiEventName()));
537     },
538
539     populateStatusMessageElement: function(element, eventData)
540     {
541         var status = WebInspector.UIString("Paused on a \"%s\" Event Listener.", this._uiEventName());
542         element.appendChild(document.createTextNode(status));
543     },
544
545     _uiEventName: function()
546     {
547         return WebInspector.EventListenerBreakpointView.eventNameForUI(this._eventName);
548     }
549 }
550
551 WebInspector.EventListenerBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
552
553 WebInspector.XHRBreakpointView = function(manager, id, enabled, url)
554 {
555     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
556     this._url = url;
557 }
558
559 WebInspector.XHRBreakpointView.prototype = {
560     compareTo: function(other)
561     {
562         return this._compare(this._url, other._url);
563     },
564
565     populateEditElement: function(element)
566     {
567         element.textContent = this._url;
568     },
569
570     populateLabelElement: function(element)
571     {
572         var label;
573         if (!this._url.length)
574             label = WebInspector.UIString("Any XHR");
575         else
576             label = WebInspector.UIString("URL contains \"%s\"", this._url);
577         element.appendChild(document.createTextNode(label));
578         element.addStyleClass("cursor-auto");
579     },
580
581     populateStatusMessageElement: function(element)
582     {
583         var status = WebInspector.UIString("Paused on a XMLHttpRequest.");
584         element.appendChild(document.createTextNode(status));
585     }
586 }
587
588 WebInspector.XHRBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
589
590 WebInspector.DOMBreakpointTypes = {
591     SubtreeModified: 0,
592     AttributeModified: 1,
593     NodeRemoved: 2
594 };
595
596 WebInspector.domBreakpointTypeLabel = function(type)
597 {
598     if (!WebInspector._DOMBreakpointTypeLabels) {
599         WebInspector._DOMBreakpointTypeLabels = {};
600         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Subtree Modified");
601         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Attribute Modified");
602         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Node Removed");
603     }
604     return WebInspector._DOMBreakpointTypeLabels[type];
605 }
606
607 WebInspector.domBreakpointTypeContextMenuLabel = function(type)
608 {
609     if (!WebInspector._DOMBreakpointTypeContextMenuLabels) {
610         WebInspector._DOMBreakpointTypeContextMenuLabels = {};
611         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Break on Subtree Modifications");
612         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Break on Attributes Modifications");
613         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Break on Node Removal");
614     }
615     return WebInspector._DOMBreakpointTypeContextMenuLabels[type];
616 }