Web Inspector: implement reverse mapping for compiler source maps.
[WebKit.git] / Source / WebCore / inspector / front-end / DebuggerPresentationModel.js
1 /*
2  * Copyright (C) 2011 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 /**
32  * @constructor
33  */
34 WebInspector.DebuggerPresentationModel = function()
35 {
36     // FIXME: apply formatter from outside as a generic mapping.
37     this._formatter = new WebInspector.ScriptFormatter();
38     this._rawSourceCode = {};
39     this._presentationCallFrames = [];
40     this._selectedCallFrameIndex = 0;
41
42     this._breakpointManager = new WebInspector.BreakpointManager(WebInspector.settings.breakpoints, this._breakpointAdded.bind(this), this._breakpointRemoved.bind(this), WebInspector.debuggerModel);
43
44     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.ParsedScriptSource, this._parsedScriptSource, this);
45     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.FailedToParseScriptSource, this._failedToParseScriptSource, this);
46     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerPaused, this._debuggerPaused, this);
47     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.DebuggerResumed, this._debuggerResumed, this);
48     WebInspector.debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.Reset, this._debuggerReset, this);
49
50     WebInspector.console.addEventListener(WebInspector.ConsoleModel.Events.MessageAdded, this._consoleMessageAdded, this);
51     WebInspector.console.addEventListener(WebInspector.ConsoleModel.Events.ConsoleCleared, this._consoleCleared, this);
52
53     new WebInspector.DebuggerPresentationModelResourceBinding(this);
54 }
55
56 WebInspector.DebuggerPresentationModel.Events = {
57     UISourceCodeAdded: "source-file-added",
58     UISourceCodeReplaced: "source-file-replaced",
59     ConsoleMessageAdded: "console-message-added",
60     ConsoleMessagesCleared: "console-messages-cleared",
61     BreakpointAdded: "breakpoint-added",
62     BreakpointRemoved: "breakpoint-removed",
63     DebuggerPaused: "debugger-paused",
64     DebuggerResumed: "debugger-resumed",
65     CallFrameSelected: "call-frame-selected"
66 }
67
68 WebInspector.DebuggerPresentationModel.prototype = {
69     linkifyLocation: function(sourceURL, lineNumber, columnNumber, classes)
70     {
71         var linkText = WebInspector.formatLinkText(sourceURL, lineNumber);
72         var anchor = WebInspector.linkifyURLAsNode(sourceURL, linkText, classes, false);
73
74         var rawSourceCode = this._rawSourceCodeForScript(sourceURL);
75         if (!rawSourceCode) {
76             anchor.setAttribute("preferred_panel", "resources");
77             anchor.setAttribute("line_number", lineNumber);
78             return anchor;
79         }
80
81         function updateAnchor()
82         {
83             var uiLocation = rawSourceCode.rawLocationToUILocation({ lineNumber: lineNumber, columnNumber: columnNumber });
84             anchor.textContent = WebInspector.formatLinkText(uiLocation.uiSourceCode.url, uiLocation.lineNumber);
85             anchor.setAttribute("preferred_panel", "scripts");
86             anchor.uiSourceCode = uiLocation.uiSourceCode;
87             anchor.lineNumber = uiLocation.lineNumber;
88         }
89         if (rawSourceCode.uiSourceCode)
90             updateAnchor.call(this);
91         rawSourceCode.addEventListener(WebInspector.RawSourceCode.Events.SourceMappingUpdated, updateAnchor, this);
92         return anchor;
93     },
94
95     _parsedScriptSource: function(event)
96     {
97         this._addScript(event.data);
98     },
99
100     _failedToParseScriptSource: function(event)
101     {
102         this._addScript(event.data);
103     },
104
105     _addScript: function(script)
106     {
107         var rawSourceCodeId = this._createRawSourceCodeId(script.sourceURL, script.scriptId);
108         var rawSourceCode = this._rawSourceCode[rawSourceCodeId];
109         if (rawSourceCode) {
110             rawSourceCode.addScript(script);
111             return;
112         }
113
114         var resource;
115         if (script.sourceURL)
116             resource = WebInspector.networkManager.inflightResourceForURL(script.sourceURL) || WebInspector.resourceForURL(script.sourceURL);
117         rawSourceCode = new WebInspector.RawSourceCode(rawSourceCodeId, script, resource, this._formatter, this._formatSource);
118         this._rawSourceCode[rawSourceCodeId] = rawSourceCode;
119         if (rawSourceCode.uiSourceCode)
120             this._updateSourceMapping(rawSourceCode, null);
121         rawSourceCode.addEventListener(WebInspector.RawSourceCode.Events.SourceMappingUpdated, this._sourceMappingUpdated, this);
122     },
123
124     _sourceMappingUpdated: function(event)
125     {
126         var rawSourceCode = event.target;
127         var oldUISourceCode = event.data.oldUISourceCode;
128         this._updateSourceMapping(rawSourceCode, oldUISourceCode);
129     },
130
131     _updateSourceMapping: function(rawSourceCode, oldUISourceCode)
132     {
133         var uiSourceCode = rawSourceCode.uiSourceCode;
134
135         if (oldUISourceCode) {
136             var breakpoints = this._breakpointManager.breakpointsForUISourceCode(oldUISourceCode);
137             for (var lineNumber in breakpoints) {
138                 var breakpoint = breakpoints[lineNumber];
139                 this._breakpointRemoved(breakpoint);
140                 delete breakpoint.uiSourceCode;
141             }
142         }
143
144         this._restoreBreakpoints(uiSourceCode);
145         this._restoreConsoleMessages(uiSourceCode);
146
147         if (!oldUISourceCode)
148             this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.UISourceCodeAdded, uiSourceCode);
149         else {
150             var eventData = { uiSourceCode: uiSourceCode, oldUISourceCode: oldUISourceCode };
151             this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.UISourceCodeReplaced, eventData);
152         }
153     },
154
155     _restoreBreakpoints: function(uiSourceCode)
156     {
157         this._breakpointManager.uiSourceCodeAdded(uiSourceCode);
158         var breakpoints = this._breakpointManager.breakpointsForUISourceCode(uiSourceCode);
159         for (var lineNumber in breakpoints)
160             this._breakpointAdded(breakpoints[lineNumber]);
161     },
162
163     _restoreConsoleMessages: function(uiSourceCode)
164     {
165         var messages = uiSourceCode.rawSourceCode.messages;
166         for (var i = 0; i < messages.length; ++i)
167             messages[i]._presentationMessage = this._createPresentationMessage(messages[i], uiSourceCode);
168     },
169
170     canEditScriptSource: function(uiSourceCode)
171     {
172         if (!Preferences.canEditScriptSource || this._formatSource)
173             return false;
174         var rawSourceCode = uiSourceCode.rawSourceCode;
175         var script = this._scriptForRawSourceCode(rawSourceCode);
176         return script && !script.lineOffset && !script.columnOffset;
177     },
178
179     setScriptSource: function(uiSourceCode, newSource, callback)
180     {
181         var rawSourceCode = uiSourceCode.rawSourceCode;
182         var script = this._scriptForRawSourceCode(rawSourceCode);
183
184         function didEditScriptSource(error)
185         {
186             callback(error);
187             if (error)
188                 return;
189
190             var resource = WebInspector.resourceForURL(rawSourceCode.url);
191             if (resource)
192                 resource.addRevision(newSource);
193
194             rawSourceCode.contentEdited();
195
196             if (WebInspector.debuggerModel.callFrames)
197                 this._debuggerPaused();
198         }
199         WebInspector.debuggerModel.setScriptSource(script.scriptId, newSource, didEditScriptSource.bind(this));
200     },
201
202     _updateBreakpointsAfterLiveEdit: function(uiSourceCode, oldSource, newSource)
203     {
204         var breakpoints = this._breakpointManager.breakpointsForUISourceCode(uiSourceCode);
205
206         // Clear and re-create breakpoints according to text diff.
207         var diff = Array.diff(oldSource.split("\n"), newSource.split("\n"));
208         for (var lineNumber in breakpoints) {
209             var breakpoint = breakpoints[lineNumber];
210
211             this.removeBreakpoint(uiSourceCode, lineNumber);
212
213             var newLineNumber = diff.left[lineNumber].row;
214             if (newLineNumber === undefined) {
215                 for (var i = lineNumber - 1; i >= 0; --i) {
216                     if (diff.left[i].row === undefined)
217                         continue;
218                     var shiftedLineNumber = diff.left[i].row + lineNumber - i;
219                     if (shiftedLineNumber < diff.right.length) {
220                         var originalLineNumber = diff.right[shiftedLineNumber].row;
221                         if (originalLineNumber === lineNumber || originalLineNumber === undefined)
222                             newLineNumber = shiftedLineNumber;
223                     }
224                     break;
225                 }
226             }
227             if (newLineNumber !== undefined)
228                 this.setBreakpoint(uiSourceCode, newLineNumber, breakpoint.condition, breakpoint.enabled);
229         }
230     },
231
232     setFormatSource: function(formatSource)
233     {
234         if (this._formatSource === formatSource)
235             return;
236
237         this._formatSource = formatSource;
238         this._breakpointManager.reset();
239         for (var id in this._rawSourceCode)
240             this._rawSourceCode[id].setFormatted(this._formatSource);
241
242         if (WebInspector.debuggerModel.callFrames)
243             this._debuggerPaused();
244     },
245
246     _consoleMessageAdded: function(event)
247     {
248         var message = event.data;
249         if (!message.url || !message.isErrorOrWarning() || !message.message)
250             return;
251
252         var rawSourceCode = this._rawSourceCodeForScript(message.url);
253         if (!rawSourceCode)
254             return;
255
256         rawSourceCode.messages.push(message);
257         if (rawSourceCode.uiSourceCode) {
258             message._presentationMessage = this._createPresentationMessage(message, rawSourceCode.uiSourceCode);
259             this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.ConsoleMessageAdded, message._presentationMessage);
260         }
261     },
262
263     _createPresentationMessage: function(message, uiSourceCode)
264     {
265         // FIXME(62725): stack trace line/column numbers are one-based.
266         var lineNumber = message.stackTrace ? message.stackTrace[0].lineNumber - 1 : message.line - 1;
267         var columnNumber = message.stackTrace ? message.stackTrace[0].columnNumber - 1 : 0;
268         var uiLocation = uiSourceCode.rawSourceCode.rawLocationToUILocation({ lineNumber: lineNumber, columnNumber: columnNumber });
269         var presentationMessage = {};
270         presentationMessage.uiSourceCode = uiLocation.uiSourceCode;
271         presentationMessage.lineNumber = uiLocation.lineNumber;
272         presentationMessage.originalMessage = message;
273         return presentationMessage;
274     },
275
276     _consoleCleared: function()
277     {
278         for (var id in this._rawSourceCode)
279             this._rawSourceCode[id].messages = [];
280         this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.ConsoleMessagesCleared);
281     },
282
283     continueToLine: function(uiSourceCode, lineNumber)
284     {
285         var rawLocation = uiSourceCode.rawSourceCode.uiLocationToRawLocation(lineNumber, 0);
286         WebInspector.debuggerModel.continueToLocation(rawLocation);
287     },
288
289     breakpointsForUISourceCode: function(uiSourceCode)
290     {
291         var breakpointsMap = this._breakpointManager.breakpointsForUISourceCode(uiSourceCode);
292         var breakpointsList = [];
293         for (var lineNumber in breakpointsMap)
294             breakpointsList.push(breakpointsMap[lineNumber]);
295         return breakpointsList;
296     },
297
298     messagesForUISourceCode: function(uiSourceCode)
299     {
300         var rawSourceCode = uiSourceCode.rawSourceCode;
301         var messages = [];
302         for (var i = 0; i < rawSourceCode.messages.length; ++i)
303             messages.push(rawSourceCode.messages[i]._presentationMessage);
304         return messages;
305     },
306
307     setBreakpoint: function(uiSourceCode, lineNumber, condition, enabled)
308     {
309         this._breakpointManager.setBreakpoint(uiSourceCode, lineNumber, condition, enabled);
310     },
311
312     setBreakpointEnabled: function(uiSourceCode, lineNumber, enabled)
313     {
314         var breakpoint = this.findBreakpoint(uiSourceCode, lineNumber);
315         if (!breakpoint)
316             return;
317         this._breakpointManager.removeBreakpoint(uiSourceCode, lineNumber);
318         this._breakpointManager.setBreakpoint(uiSourceCode, lineNumber, breakpoint.condition, enabled);
319     },
320
321     updateBreakpoint: function(uiSourceCode, lineNumber, condition, enabled)
322     {
323         this._breakpointManager.removeBreakpoint(uiSourceCode, lineNumber);
324         this._breakpointManager.setBreakpoint(uiSourceCode, lineNumber, condition, enabled);
325     },
326
327     removeBreakpoint: function(uiSourceCode, lineNumber)
328     {
329         this._breakpointManager.removeBreakpoint(uiSourceCode, lineNumber);
330     },
331
332     findBreakpoint: function(uiSourceCode, lineNumber)
333     {
334         return this._breakpointManager.breakpointsForUISourceCode(uiSourceCode)[lineNumber];
335     },
336
337     _breakpointAdded: function(breakpoint)
338     {
339         if (breakpoint.uiSourceCode)
340             this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.BreakpointAdded, breakpoint);
341     },
342
343     _breakpointRemoved: function(breakpoint)
344     {
345         if (breakpoint.uiSourceCode)
346             this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.BreakpointRemoved, breakpoint);
347     },
348
349     _debuggerPaused: function()
350     {
351         var callFrames = WebInspector.debuggerModel.callFrames;
352         this._presentationCallFrames = [];
353         for (var i = 0; i < callFrames.length; ++i) {
354             var callFrame = callFrames[i];
355             var rawSourceCode;
356             var script = WebInspector.debuggerModel.scriptForSourceID(callFrame.location.scriptId);
357             if (script)
358                 rawSourceCode = this._rawSourceCodeForScript(script.sourceURL, script.scriptId);
359             this._presentationCallFrames.push(new WebInspector.PresentationCallFrame(callFrame, i, this, rawSourceCode));
360         }
361         var details = WebInspector.debuggerModel.debuggerPausedDetails;
362         this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.DebuggerPaused, { callFrames: this._presentationCallFrames, details: details });
363
364         this.selectedCallFrame = this._presentationCallFrames[this._selectedCallFrameIndex];
365     },
366
367     _debuggerResumed: function()
368     {
369         this._presentationCallFrames = [];
370         this._selectedCallFrameIndex = 0;
371         this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.DebuggerResumed);
372     },
373
374     set selectedCallFrame(callFrame)
375     {
376         this._selectedCallFrameIndex = callFrame.index;
377         callFrame.select();
378         this.dispatchEventToListeners(WebInspector.DebuggerPresentationModel.Events.CallFrameSelected, callFrame);
379     },
380
381     get selectedCallFrame()
382     {
383         return this._presentationCallFrames[this._selectedCallFrameIndex];
384     },
385
386     _rawSourceCodeForScript: function(sourceURL, scriptId)
387     {
388         if (!sourceURL) {
389             var script = WebInspector.debuggerModel.scriptForSourceID(scriptId);
390             if (!script)
391                 return;
392             sourceURL = script.sourceURL;
393         }
394         return this._rawSourceCode[this._createRawSourceCodeId(sourceURL, scriptId)];
395     },
396
397     _scriptForRawSourceCode: function(rawSourceCode)
398     {
399         function filter(script)
400         {
401             return this._createRawSourceCodeId(script.sourceURL, script.scriptId) === rawSourceCode.id;
402         }
403         return WebInspector.debuggerModel.queryScripts(filter.bind(this))[0];
404     },
405
406     _createRawSourceCodeId: function(sourceURL, scriptId)
407     {
408         return sourceURL || scriptId;
409     },
410
411     _debuggerReset: function()
412     {
413         this._rawSourceCode = {};
414         this._presentationCallFrames = [];
415         this._selectedCallFrameIndex = 0;
416         this._breakpointManager.debuggerReset();
417     }
418 }
419
420 WebInspector.DebuggerPresentationModel.prototype.__proto__ = WebInspector.Object.prototype;
421
422 /**
423  * @constructor
424  */
425 WebInspector.PresentationCallFrame = function(callFrame, index, model, rawSourceCode)
426 {
427     this._callFrame = callFrame;
428     this._index = index;
429     this._model = model;
430     this._rawSourceCode = rawSourceCode;
431     this._script = WebInspector.debuggerModel.scriptForSourceID(callFrame.location.scriptId);
432 }
433
434 WebInspector.PresentationCallFrame.prototype = {
435     get functionName()
436     {
437         return this._callFrame.functionName;
438     },
439
440     get type()
441     {
442         return this._callFrame.type;
443     },
444
445     get isInternalScript()
446     {
447         return !this._script;
448     },
449
450     get url()
451     {
452         if (this._rawSourceCode && this._rawSourceCode.uiSourceCode)
453             return this._rawSourceCode.uiSourceCode.url;
454     },
455
456     get scopeChain()
457     {
458         return this._callFrame.scopeChain;
459     },
460
461     get this()
462     {
463         return this._callFrame.this;
464     },
465
466     get index()
467     {
468         return this._index;
469     },
470
471     select: function()
472     {
473         if (this._rawSourceCode)
474             this._rawSourceCode.forceUpdateSourceMapping();
475     },
476
477     evaluate: function(code, objectGroup, includeCommandLineAPI, returnByValue, callback)
478     {
479         function didEvaluateOnCallFrame(error, result, wasThrown)
480         {
481             if (error) {
482                 console.error(error);
483                 callback(null);
484                 return;
485             }
486
487             if (returnByValue && !wasThrown)
488                 callback(result, wasThrown);
489             else
490                 callback(WebInspector.RemoteObject.fromPayload(result), wasThrown);
491         }
492         DebuggerAgent.evaluateOnCallFrame(this._callFrame.id, code, objectGroup, includeCommandLineAPI, returnByValue, didEvaluateOnCallFrame.bind(this));
493     },
494
495     sourceLine: function(callback)
496     {
497         var rawLocation = this._callFrame.location;
498         if (!this._rawSourceCode) {
499             callback(undefined, rawLocation.lineNumber);
500             return;
501         }
502
503         if (this._rawSourceCode.uiSourceCode) {
504             var uiLocation = this._rawSourceCode.rawLocationToUILocation(rawLocation);
505             callback(uiLocation.uiSourceCode, uiLocation.lineNumber);
506             return;
507         }
508
509         function sourceMappingUpdated()
510         {
511             this._rawSourceCode.removeEventListener(WebInspector.RawSourceCode.Events.SourceMappingUpdated, sourceMappingUpdated, this);
512             var uiLocation = this._rawSourceCode.rawLocationToUILocation(rawLocation);
513             callback(uiLocation.uiSourceCode, uiLocation.lineNumber);
514         }
515         this._rawSourceCode.addEventListener(WebInspector.RawSourceCode.Events.SourceMappingUpdated, sourceMappingUpdated, this);
516     }
517 }
518
519 /**
520  * @constructor
521  * @extends {WebInspector.ResourceDomainModelBinding}
522  */
523 WebInspector.DebuggerPresentationModelResourceBinding = function(model)
524 {
525     this._presentationModel = model;
526     WebInspector.Resource.registerDomainModelBinding(WebInspector.Resource.Type.Script, this);
527 }
528
529 WebInspector.DebuggerPresentationModelResourceBinding.prototype = {
530     canSetContent: function(resource)
531     {
532         var rawSourceCode = this._presentationModel._rawSourceCodeForScript(resource.url)
533         if (!rawSourceCode)
534             return false;
535         return this._presentationModel.canEditScriptSource(rawSourceCode.uiSourceCode);
536     },
537
538     setContent: function(resource, content, majorChange, userCallback)
539     {
540         if (!majorChange)
541             return;
542
543         var rawSourceCode = this._presentationModel._rawSourceCodeForScript(resource.url);
544         if (!rawSourceCode) {
545             userCallback("Resource is not editable");
546             return;
547         }
548
549         resource.requestContent(this._setContentWithInitialContent.bind(this, rawSourceCode.uiSourceCode, content, userCallback));
550     },
551
552     _setContentWithInitialContent: function(uiSourceCode, content, userCallback, oldContent)
553     {
554         function callback(error)
555         {
556             if (userCallback)
557                 userCallback(error);
558             if (!error)
559                 this._presentationModel._updateBreakpointsAfterLiveEdit(uiSourceCode, oldContent, content);
560         }
561         this._presentationModel.setScriptSource(uiSourceCode, content, callback.bind(this));
562     }
563 }
564
565 WebInspector.DebuggerPresentationModelResourceBinding.prototype.__proto__ = WebInspector.ResourceDomainModelBinding.prototype;
566
567 /**
568  * @type {?WebInspector.DebuggerPresentationModel}
569  */
570 WebInspector.debuggerPresentationModel = null;