2011-02-01 Pavel Podivilov <podivilov@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             InspectorBackend.setAllBrowserBreakpoints(this._stickyBreakpoints);
211             this._breakpointsPushedToFrontend = true;
212         }
213     },
214
215     restoreDOMBreakpoints: function()
216     {
217         function didPushNodeByPathToFrontend(path, nodeId)
218         {
219             pathToNodeId[path] = nodeId;
220             pendingCalls -= 1;
221             if (pendingCalls)
222                 return;
223             for (var i = 0; i < breakpoints.length; ++i) {
224                 var breakpoint = breakpoints[i];
225                 if (breakpoint.type !== WebInspector.BreakpointManager.BreakpointTypes.DOM)
226                     continue;
227                 var nodeId = pathToNodeId[breakpoint.condition.path];
228                 if (nodeId)
229                     this._createDOMBreakpoint(nodeId, breakpoint.condition.type, breakpoint.enabled, true);
230             }
231             this._domBreakpointsRestored = true;
232             this._saveBreakpoints();
233         }
234
235         var breakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
236         var pathToNodeId = {};
237         var pendingCalls = 0;
238         for (var i = 0; i < breakpoints.length; ++i) {
239             if (breakpoints[i].type !== WebInspector.BreakpointManager.BreakpointTypes.DOM)
240                 continue;
241             var path = breakpoints[i].condition.path;
242             if (path in pathToNodeId)
243                 continue;
244             pathToNodeId[path] = 0;
245             pendingCalls += 1;
246             InspectorBackend.pushNodeByPathToFrontend(path, didPushNodeByPathToFrontend.bind(this, path));
247         }
248         if (!pendingCalls)
249             this._domBreakpointsRestored = true;
250     },
251
252     _saveBreakpoints: function()
253     {
254         var breakpoints = [];
255         for (var breakpointId in this._breakpoints) {
256             var breakpoint = this._breakpoints[breakpointId];
257             var persistentBreakpoint = breakpoint._serializeToJSON();
258             persistentBreakpoint.enabled = breakpoint.enabled;
259             breakpoints.push(persistentBreakpoint);
260         }
261         if (!this._domBreakpointsRestored) {
262             var stickyBreakpoints = this._stickyBreakpoints[WebInspector.settings.projectId] || [];
263             for (var i = 0; i < stickyBreakpoints.length; ++i) {
264                 if (stickyBreakpoints[i].type === WebInspector.BreakpointManager.BreakpointTypes.DOM)
265                     breakpoints.push(stickyBreakpoints[i]);
266             }
267         }
268         WebInspector.settings.nativeBreakpoints = breakpoints;
269
270         this._stickyBreakpoints[WebInspector.settings.projectId] = breakpoints;
271         InspectorBackend.setAllBrowserBreakpoints(this._stickyBreakpoints);
272     },
273
274     _validateBreakpoints: function(persistentBreakpoints)
275     {
276         var breakpoints = [];
277         var breakpointsSet = {};
278         for (var i = 0; i < persistentBreakpoints.length; ++i) {
279             var breakpoint = persistentBreakpoints[i];
280             if (!("type" in breakpoint && "enabled" in breakpoint && "condition" in breakpoint))
281                 continue;
282             var id = breakpoint.type + ":";
283             var condition = breakpoint.condition;
284             if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.DOM) {
285                 if (typeof condition.path !== "string" || typeof condition.type !== "number")
286                     continue;
287                 id += condition.path + ":" + condition.type;
288             } else if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.EventListener) {
289                 if (typeof condition.eventName !== "string")
290                     continue;
291                 id += condition.eventName;
292             } else if (breakpoint.type === WebInspector.BreakpointManager.BreakpointTypes.XHR) {
293                 if (typeof condition.url !== "string")
294                     continue;
295                 id += condition.url;
296             } else
297                 continue;
298             if (id in breakpointsSet)
299                 continue;
300             breakpointsSet[id] = true;
301             breakpoints.push(breakpoint);
302         }
303         return breakpoints;
304     },
305
306     _createDOMBreakpointId: function(nodeId, type)
307     {
308         return "dom:" + nodeId + ":" + type;
309     },
310
311     _createEventListenerBreakpointId: function(eventName)
312     {
313         return "eventListner:" + eventName;
314     },
315
316     _createXHRBreakpointId: function(url)
317     {
318         return "xhr:" + url;
319     }
320 }
321
322 WebInspector.BreakpointManager.prototype.__proto__ = WebInspector.Object.prototype;
323
324 WebInspector.DOMBreakpoint = function(node, type)
325 {
326     this._nodeId = node.id;
327     this._path = node.path();
328     this._type = type;
329 }
330
331 WebInspector.DOMBreakpoint.prototype = {
332     _enable: function()
333     {
334         InspectorBackend.setDOMBreakpoint(this._nodeId, this._type);
335     },
336
337     _disable: function()
338     {
339         InspectorBackend.removeDOMBreakpoint(this._nodeId, this._type);
340     },
341
342     _serializeToJSON: function()
343     {
344         var type = WebInspector.BreakpointManager.BreakpointTypes.DOM;
345         return { type: type, condition: { path: this._path, type: this._type } };
346     }
347 }
348
349 WebInspector.EventListenerBreakpoint = function(eventName)
350 {
351     this._eventName = eventName;
352 }
353
354 WebInspector.EventListenerBreakpoint.prototype = {
355     _enable: function()
356     {
357         InspectorBackend.setEventListenerBreakpoint(this._eventName);
358     },
359
360     _disable: function()
361     {
362         InspectorBackend.removeEventListenerBreakpoint(this._eventName);
363     },
364
365     _serializeToJSON: function()
366     {
367         var type = WebInspector.BreakpointManager.BreakpointTypes.EventListener;
368         return { type: type, condition: { eventName: this._eventName } };
369     }
370 }
371
372 WebInspector.XHRBreakpoint = function(url)
373 {
374     this._url = url;
375 }
376
377 WebInspector.XHRBreakpoint.prototype = {
378     _enable: function()
379     {
380         InspectorBackend.setXHRBreakpoint(this._url);
381     },
382
383     _disable: function()
384     {
385         InspectorBackend.removeXHRBreakpoint(this._url);
386     },
387
388     _serializeToJSON: function()
389     {
390         var type = WebInspector.BreakpointManager.BreakpointTypes.XHR;
391         return { type: type, condition: { url: this._url } };
392     }
393 }
394
395
396
397 WebInspector.NativeBreakpointView = function(manager, id, enabled)
398 {
399     this._manager = manager;
400     this._id = id;
401     this._enabled = enabled;
402     this._hit = false;
403 }
404
405 WebInspector.NativeBreakpointView.prototype = {
406     get enabled()
407     {
408         return this._enabled;
409     },
410
411     set enabled(enabled)
412     {
413         this._manager._setBreakpointEnabled(this._id, enabled);
414         this._enabled = enabled;
415         this.dispatchEventToListeners("enable-changed");
416     },
417
418     get hit()
419     {
420         return this._hit;
421     },
422
423     set hit(hit)
424     {
425         this._hit = hit;
426         this.dispatchEventToListeners("hit-state-changed");
427     },
428
429     remove: function()
430     {
431         this._manager._removeBreakpoint(this._id);
432         this._onRemove();
433         this.dispatchEventToListeners("removed");
434     },
435
436     _compare: function(x, y)
437     {
438         if (x !== y)
439             return x < y ? -1 : 1;
440         return 0;
441     },
442
443     _onRemove: function()
444     {
445     }
446 }
447
448 WebInspector.NativeBreakpointView.prototype.__proto__ = WebInspector.Object.prototype;
449
450 WebInspector.DOMBreakpointView = function(manager, id, enabled, node, type)
451 {
452     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
453     this._node = node;
454     this._nodeId = node.id;
455     this._type = type;
456     node.breakpoints[this._type] = this;
457 }
458
459 WebInspector.DOMBreakpointView.prototype = {
460     compareTo: function(other)
461     {
462         return this._compare(this._type, other._type);
463     },
464
465     populateLabelElement: function(element)
466     {
467         // FIXME: this should belong to the view, not the manager.
468         var linkifiedNode = WebInspector.panels.elements.linkifyNodeById(this._nodeId);
469         linkifiedNode.addStyleClass("monospace");
470         element.appendChild(linkifiedNode);
471         var description = document.createElement("div");
472         description.className = "source-text";
473         description.textContent = WebInspector.domBreakpointTypeLabel(this._type);
474         element.appendChild(description);
475     },
476
477     populateStatusMessageElement: function(element, eventData)
478     {
479         var substitutions = [WebInspector.domBreakpointTypeLabel(this._type), WebInspector.panels.elements.linkifyNodeById(this._nodeId)];
480         var formatters = {
481             s: function(substitution)
482             {
483                 return substitution;
484             }
485         };
486         function append(a, b)
487         {
488             if (typeof b === "string")
489                 b = document.createTextNode(b);
490             element.appendChild(b);
491         }
492         if (this._type === WebInspector.DOMBreakpointTypes.SubtreeModified) {
493             var targetNode = WebInspector.panels.elements.linkifyNodeById(eventData.targetNodeId);
494             if (eventData.insertion) {
495                 if (eventData.targetNodeId !== this._nodeId)
496                     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);
497                 else
498                     WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s, because a new child was added to that node.", substitutions, formatters, "", append);
499             } else
500                 WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s, because its descendant %s was removed.", substitutions.concat(targetNode), formatters, "", append);
501         } else
502             WebInspector.formatLocalized("Paused on a \"%s\" breakpoint set on %s.", substitutions, formatters, "", append);
503     },
504
505     _onRemove: function()
506     {
507         delete this._node.breakpoints[this._type];
508     }
509 }
510
511 WebInspector.DOMBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
512
513 WebInspector.EventListenerBreakpointView = function(manager, id, enabled, eventName)
514 {
515     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
516     this._eventName = eventName;
517 }
518
519 WebInspector.EventListenerBreakpointView.eventNameForUI = function(eventName)
520 {
521     if (!WebInspector.EventListenerBreakpointView._eventNamesForUI) {
522         WebInspector.EventListenerBreakpointView._eventNamesForUI = {
523             "instrumentation:setTimer": WebInspector.UIString("Set Timer"),
524             "instrumentation:clearTimer": WebInspector.UIString("Clear Timer"),
525             "instrumentation:timerFired": WebInspector.UIString("Timer Fired")
526         };
527     }
528     return WebInspector.EventListenerBreakpointView._eventNamesForUI[eventName] || eventName.substring(eventName.indexOf(":") + 1);
529 }
530
531 WebInspector.EventListenerBreakpointView.prototype = {
532     get eventName()
533     {
534         return this._eventName;
535     },
536
537     compareTo: function(other)
538     {
539         return this._compare(this._eventName, other._eventName);
540     },
541
542     populateLabelElement: function(element)
543     {
544         element.appendChild(document.createTextNode(this._uiEventName()));
545     },
546
547     populateStatusMessageElement: function(element, eventData)
548     {
549         var status = WebInspector.UIString("Paused on a \"%s\" Event Listener.", this._uiEventName());
550         element.appendChild(document.createTextNode(status));
551     },
552
553     _uiEventName: function()
554     {
555         return WebInspector.EventListenerBreakpointView.eventNameForUI(this._eventName);
556     }
557 }
558
559 WebInspector.EventListenerBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
560
561 WebInspector.XHRBreakpointView = function(manager, id, enabled, url)
562 {
563     WebInspector.NativeBreakpointView.call(this, manager, id, enabled);
564     this._url = url;
565 }
566
567 WebInspector.XHRBreakpointView.prototype = {
568     compareTo: function(other)
569     {
570         return this._compare(this._url, other._url);
571     },
572
573     populateEditElement: function(element)
574     {
575         element.textContent = this._url;
576     },
577
578     populateLabelElement: function(element)
579     {
580         var label;
581         if (!this._url.length)
582             label = WebInspector.UIString("Any XHR");
583         else
584             label = WebInspector.UIString("URL contains \"%s\"", this._url);
585         element.appendChild(document.createTextNode(label));
586         element.addStyleClass("cursor-auto");
587     },
588
589     populateStatusMessageElement: function(element)
590     {
591         var status = WebInspector.UIString("Paused on a XMLHttpRequest.");
592         element.appendChild(document.createTextNode(status));
593     }
594 }
595
596 WebInspector.XHRBreakpointView.prototype.__proto__ = WebInspector.NativeBreakpointView.prototype;
597
598 WebInspector.DOMBreakpointTypes = {
599     SubtreeModified: 0,
600     AttributeModified: 1,
601     NodeRemoved: 2
602 };
603
604 WebInspector.domBreakpointTypeLabel = function(type)
605 {
606     if (!WebInspector._DOMBreakpointTypeLabels) {
607         WebInspector._DOMBreakpointTypeLabels = {};
608         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Subtree Modified");
609         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Attribute Modified");
610         WebInspector._DOMBreakpointTypeLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Node Removed");
611     }
612     return WebInspector._DOMBreakpointTypeLabels[type];
613 }
614
615 WebInspector.domBreakpointTypeContextMenuLabel = function(type)
616 {
617     if (!WebInspector._DOMBreakpointTypeContextMenuLabels) {
618         WebInspector._DOMBreakpointTypeContextMenuLabels = {};
619         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.SubtreeModified] = WebInspector.UIString("Break on Subtree Modifications");
620         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.AttributeModified] = WebInspector.UIString("Break on Attributes Modifications");
621         WebInspector._DOMBreakpointTypeContextMenuLabels[WebInspector.DOMBreakpointTypes.NodeRemoved] = WebInspector.UIString("Break on Node Removal");
622     }
623     return WebInspector._DOMBreakpointTypeContextMenuLabels[type];
624 }