d92a2d31ae41eeb8c1ef5a2d455ea72942856693
[WebKit-https.git] / Source / WebCore / inspector / front-end / ScriptSnippetModel.js
1 /*
2  * Copyright (C) 2012 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  * @extends {WebInspector.Object}
34  * @param {WebInspector.Workspace} workspace
35  */
36 WebInspector.ScriptSnippetModel = function(workspace)
37 {
38     this._workspace = workspace;
39     this._uiSourceCodeForScriptId = {};
40     this._scriptForUISourceCode = new Map();
41     this._snippetJavaScriptSourceForSnippetId = {};
42     
43     this._snippetStorage = new WebInspector.SnippetStorage("script", "Script snippet #");
44     this._lastSnippetEvaluationIndexSetting = WebInspector.settings.createSetting("lastSnippetEvaluationIndex", 0);
45     this._snippetScriptMapping = new WebInspector.SnippetScriptMapping(this);
46     this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectWillReset, this._reset, this);
47     this._loadSnippets();
48 }
49
50 WebInspector.ScriptSnippetModel.snippetSourceURLPrefix = "snippets:///";
51
52 WebInspector.ScriptSnippetModel.prototype = {
53     /**
54      * @return {WebInspector.SnippetScriptMapping}
55      */
56     get scriptMapping()
57     {
58         return this._snippetScriptMapping;
59     },
60
61     _loadSnippets: function()
62     {
63         var snippets = this._snippetStorage.snippets();
64         for (var i = 0; i < snippets.length; ++i)
65             this._addScriptSnippet(snippets[i]);
66     },
67
68     /**
69      * @return {WebInspector.SnippetJavaScriptSource}
70      */
71     createScriptSnippet: function()
72     {
73         var snippet = this._snippetStorage.createSnippet();
74         return this._addScriptSnippet(snippet);
75     },
76
77     /**
78      * @param {WebInspector.Snippet} snippet
79      * @return {WebInspector.SnippetJavaScriptSource}
80      */
81     _addScriptSnippet: function(snippet)
82     {
83         var snippetJavaScriptSource = new WebInspector.SnippetJavaScriptSource(snippet.id, snippet.name, new WebInspector.SnippetContentProvider(snippet), this);
84         this._snippetJavaScriptSourceForSnippetId[snippet.id] = snippetJavaScriptSource;
85         this._workspace.project().addUISourceCode(snippetJavaScriptSource);
86         return snippetJavaScriptSource;
87     },
88
89     /**
90      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
91      */
92     deleteScriptSnippet: function(snippetJavaScriptSource)
93     {
94         var snippet = this._snippetStorage.snippetForId(snippetJavaScriptSource.snippetId);
95         this._snippetStorage.deleteSnippet(snippet);
96         this._removeBreakpoints(snippetJavaScriptSource);
97         this._releaseSnippetScript(snippetJavaScriptSource);
98         delete this._snippetJavaScriptSourceForSnippetId[snippet.id];
99         this._workspace.project().removeUISourceCode(snippetJavaScriptSource);
100     },
101
102     /**
103      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
104      * @param {string} newName
105      */
106     renameScriptSnippet: function(snippetJavaScriptSource, newName)
107     {
108         var snippet = this._snippetStorage.snippetForId(snippetJavaScriptSource.snippetId);
109         if (!snippet || !newName || snippet.name === newName)
110             return;
111         snippet.name = newName;
112         snippetJavaScriptSource.urlChanged(snippet.name);
113     },
114
115     /**
116      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
117      * @return {boolean}
118      */
119     _isDivergedFromVM: function(snippetJavaScriptSource)
120     {
121         var script = this._scriptForUISourceCode.get(snippetJavaScriptSource);
122         return !script;
123     },
124
125     /**
126      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
127      * @param {string} newContent
128      */
129     _setScriptSnippetContent: function(snippetJavaScriptSource, newContent)
130     {
131         var snippet = this._snippetStorage.snippetForId(snippetJavaScriptSource.snippetId);
132         snippet.content = newContent;
133     },
134
135     /**
136      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
137      */
138     _scriptSnippetEdited: function(snippetJavaScriptSource)
139     {
140         var script = this._scriptForUISourceCode.get(snippetJavaScriptSource);
141         if (!script)
142             return;
143         
144         var breakpointLocations = this._removeBreakpoints(snippetJavaScriptSource);
145         this._releaseSnippetScript(snippetJavaScriptSource);
146         this._restoreBreakpoints(snippetJavaScriptSource, breakpointLocations);
147     },
148
149     /**
150      * @param {string} snippetId
151      * @return {number}
152      */
153     _nextEvaluationIndex: function(snippetId)
154     {
155         var evaluationIndex = this._lastSnippetEvaluationIndexSetting.get() + 1;
156         this._lastSnippetEvaluationIndexSetting.set(evaluationIndex);
157         return evaluationIndex;
158     },
159
160     /**
161      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
162      */
163     evaluateScriptSnippet: function(snippetJavaScriptSource)
164     {
165         var breakpointLocations = this._removeBreakpoints(snippetJavaScriptSource);
166         this._releaseSnippetScript(snippetJavaScriptSource);
167         this._restoreBreakpoints(snippetJavaScriptSource, breakpointLocations);
168         var evaluationIndex = this._nextEvaluationIndex(snippetJavaScriptSource.snippetId);
169         snippetJavaScriptSource._evaluationIndex = evaluationIndex;
170         var evaluationUrl = this._evaluationSourceURL(snippetJavaScriptSource);
171
172         var expression = snippetJavaScriptSource.workingCopy();
173         
174         // In order to stop on the breakpoints during the snippet evaluation we need to compile and run it separately.
175         // If separate compilation and execution is not supported by the port we fall back to evaluation in console.
176         // In case we don't need that since debugger is already paused.
177         // We do the same when we are stopped on the call frame  since debugger is already paused and can not stop on breakpoint anymore.
178         if (WebInspector.debuggerModel.selectedCallFrame() || !Capabilities.separateScriptCompilationAndExecutionEnabled) {
179             expression = snippetJavaScriptSource.workingCopy() + "\n//@ sourceURL=" + evaluationUrl + "\n";
180             WebInspector.evaluateInConsole(expression, true);
181             return;
182         }
183         
184         WebInspector.showConsole();
185         DebuggerAgent.compileScript(expression, evaluationUrl, compileCallback.bind(this));
186
187         /**
188          * @param {?string} error
189          * @param {string=} scriptId
190          * @param {string=} syntaxErrorMessage
191          */
192         function compileCallback(error, scriptId, syntaxErrorMessage)
193         {
194             if (!snippetJavaScriptSource || snippetJavaScriptSource._evaluationIndex !== evaluationIndex)
195                 return;
196
197             if (error) {
198                 console.error(error);
199                 return;
200             }
201
202             if (!scriptId) {
203                 var consoleMessage = WebInspector.ConsoleMessage.create(
204                         WebInspector.ConsoleMessage.MessageSource.JS,
205                         WebInspector.ConsoleMessage.MessageLevel.Error,
206                         syntaxErrorMessage || "");
207                 WebInspector.console.addMessage(consoleMessage);
208                 return;
209             }
210
211             var breakpointLocations = this._removeBreakpoints(snippetJavaScriptSource);
212             this._restoreBreakpoints(snippetJavaScriptSource, breakpointLocations);
213
214             WebInspector.consoleView.runScript(scriptId);
215         }
216     },
217
218     /**
219      * @param {WebInspector.DebuggerModel.Location} rawLocation
220      * @return {WebInspector.UILocation}
221      */
222     _rawLocationToUILocation: function(rawLocation)
223     {
224         var uiSourceCode = this._uiSourceCodeForScriptId[rawLocation.scriptId];
225         return new WebInspector.UILocation(uiSourceCode, rawLocation.lineNumber, rawLocation.columnNumber || 0);
226     },
227
228     /**
229      * @param {WebInspector.UISourceCode} uiSourceCode
230      * @param {number} lineNumber
231      * @param {number} columnNumber
232      * @return {WebInspector.DebuggerModel.Location}
233      */
234     _uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
235     {
236         var script = this._scriptForUISourceCode.get(uiSourceCode);
237         if (!script)
238             return null;
239
240         return WebInspector.debuggerModel.createRawLocation(script, lineNumber, columnNumber);
241     },
242
243     /**
244      * @return {Array.<WebInspector.UISourceCode>}
245      */
246     _releasedUISourceCodes: function()
247     {
248         var result = [];
249         for (var scriptId in this._uiSourceCodeForScriptId) {
250             var uiSourceCode = this._uiSourceCodeForScriptId[scriptId];
251             if (!uiSourceCode.isSnippet)
252                 result.push(uiSourceCode);
253         }
254         return result;
255     },
256
257     /**
258      * @param {WebInspector.Script} script
259      */
260     _addScript: function(script)
261     {
262         var snippetId = this._snippetIdForSourceURL(script.sourceURL);
263         var snippetJavaScriptSource = this._snippetJavaScriptSourceForSnippetId[snippetId];
264         
265         if (!snippetJavaScriptSource || this._evaluationSourceURL(snippetJavaScriptSource) !== script.sourceURL) {
266             this._createUISourceCodeForScript(script);
267             return;
268         }
269         
270         console.assert(!this._scriptForUISourceCode.get(snippetJavaScriptSource));
271         this._uiSourceCodeForScriptId[script.scriptId] = snippetJavaScriptSource;
272         this._scriptForUISourceCode.put(snippetJavaScriptSource, script);
273         script.setSourceMapping(this._snippetScriptMapping);
274     },
275
276     /**
277      * @param {WebInspector.Script} script
278      */
279     _createUISourceCodeForScript: function(script)
280     {
281         var uiSourceCode = new WebInspector.JavaScriptSource(script.sourceURL, null, script, this._snippetScriptMapping, false);
282         uiSourceCode.isSnippetEvaluation = true;
283         this._uiSourceCodeForScriptId[script.scriptId] = uiSourceCode;
284         this._scriptForUISourceCode.put(uiSourceCode, script);
285         script.setSourceMapping(this._snippetScriptMapping);
286     },
287
288     /**
289      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
290      * @return {Array.<Object>}
291      */
292     _removeBreakpoints: function(snippetJavaScriptSource)
293     {
294         var breakpointLocations = WebInspector.breakpointManager.breakpointLocationsForUISourceCode(snippetJavaScriptSource);
295         for (var i = 0; i < breakpointLocations.length; ++i)
296             breakpointLocations[i].breakpoint.remove();
297         return breakpointLocations;
298     },
299
300     /**
301      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
302      * @param {Array.<Object>} breakpointLocations
303      */
304     _restoreBreakpoints: function(snippetJavaScriptSource, breakpointLocations)
305     {
306         for (var i = 0; i < breakpointLocations.length; ++i) {
307             var uiLocation = breakpointLocations[i].uiLocation;
308             var breakpoint = breakpointLocations[i].breakpoint;
309             WebInspector.breakpointManager.setBreakpoint(uiLocation.uiSourceCode, uiLocation.lineNumber, breakpoint.condition(), breakpoint.enabled());
310         }
311     },
312
313     /**
314      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
315      */
316     _releaseSnippetScript: function(snippetJavaScriptSource)
317     {
318         var script = this._scriptForUISourceCode.get(snippetJavaScriptSource);
319         if (!script)
320             return;
321
322         delete this._uiSourceCodeForScriptId[script.scriptId];
323         this._scriptForUISourceCode.remove(snippetJavaScriptSource);
324         delete snippetJavaScriptSource._evaluationIndex;
325         this._createUISourceCodeForScript(script);
326     },
327
328     /**
329      * @param {WebInspector.SnippetJavaScriptSource} snippetJavaScriptSource
330      * @return {string}
331      */
332     _evaluationSourceURL: function(snippetJavaScriptSource)
333     {
334         var snippetPrefix = WebInspector.ScriptSnippetModel.snippetSourceURLPrefix;
335         var evaluationSuffix = "_" + snippetJavaScriptSource._evaluationIndex;
336         return snippetPrefix + snippetJavaScriptSource.snippetId + evaluationSuffix;
337     },
338
339     /**
340      * @param {string} sourceURL
341      * @return {string|null}
342      */
343     _snippetIdForSourceURL: function(sourceURL)
344     {
345         var snippetPrefix = WebInspector.ScriptSnippetModel.snippetSourceURLPrefix;
346         if (!sourceURL.startsWith(snippetPrefix))
347             return null;
348         var splittedURL = sourceURL.substring(snippetPrefix.length).split("_");
349         var snippetId = splittedURL[0];
350         return snippetId;
351     },
352
353     _reset: function()
354     {
355         var removedUISourceCodes = this._releasedUISourceCodes();
356         this._uiSourceCodeForScriptId = {};
357         this._scriptForUISourceCode = new Map();
358         this._snippetJavaScriptSourceForSnippetId = {};
359         setTimeout(this._loadSnippets.bind(this), 0);
360     }
361 }
362
363 WebInspector.ScriptSnippetModel.prototype.__proto__ = WebInspector.Object.prototype;
364
365 /**
366  * @constructor
367  * @extends {WebInspector.JavaScriptSource}
368  * @param {string} snippetId
369  * @param {string} snippetName
370  * @param {WebInspector.ContentProvider} contentProvider
371  * @param {WebInspector.ScriptSnippetModel} scriptSnippetModel
372  */
373 WebInspector.SnippetJavaScriptSource = function(snippetId, snippetName, contentProvider, scriptSnippetModel)
374 {
375     WebInspector.JavaScriptSource.call(this, snippetName, null, contentProvider, scriptSnippetModel.scriptMapping, true);
376     this._snippetId = snippetId;
377     this._scriptSnippetModel = scriptSnippetModel;
378     this.isSnippet = true;
379 }
380
381 WebInspector.SnippetJavaScriptSource.prototype = {
382     /**
383      * @return {boolean}
384      */
385     isEditable: function()
386     {
387         return true;
388     },
389
390     /**
391      * @return {boolean}
392      */
393     isDivergedFromVM: function()
394     {
395         return this._scriptSnippetModel._isDivergedFromVM(this);
396     },
397
398     /**
399      * @param {function(?string)} callback
400      */
401     workingCopyCommitted: function(callback)
402     {  
403         this._scriptSnippetModel._setScriptSnippetContent(this, this.workingCopy());
404         callback(null);
405     },
406
407     workingCopyChanged: function()
408     {  
409         this._scriptSnippetModel._scriptSnippetEdited(this);
410     },
411
412     evaluate: function()
413     {
414         this._scriptSnippetModel.evaluateScriptSnippet(this);
415     },
416
417     /**
418      * @return {boolean}
419      */
420     supportsEnabledBreakpointsWhileEditing: function()
421     {
422         return true;
423     },
424
425     /**
426      * @return {string}
427      */
428     breakpointStorageId: function()
429     {
430         return WebInspector.ScriptSnippetModel.snippetSourceURLPrefix + this.snippetId;
431     },
432
433     /**
434      * @return {string}
435      */
436     get snippetId()
437     {
438         return this._snippetId;
439     }
440 }
441
442 WebInspector.SnippetJavaScriptSource.prototype.__proto__ = WebInspector.JavaScriptSource.prototype;
443
444 /**
445  * @constructor
446  * @implements {WebInspector.SourceMapping}
447  * @param {WebInspector.ScriptSnippetModel} scriptSnippetModel
448  */
449 WebInspector.SnippetScriptMapping = function(scriptSnippetModel)
450 {
451     this._scriptSnippetModel = scriptSnippetModel;
452 }
453
454 WebInspector.SnippetScriptMapping.prototype = {
455     /**
456      * @param {WebInspector.RawLocation} rawLocation
457      * @return {WebInspector.UILocation}
458      */
459     rawLocationToUILocation: function(rawLocation)
460     {
461         var debuggerModelLocation = /** @type {WebInspector.DebuggerModel.Location} */ rawLocation;
462         return this._scriptSnippetModel._rawLocationToUILocation(debuggerModelLocation);
463     },
464
465     /**
466      * @param {WebInspector.UISourceCode} uiSourceCode
467      * @param {number} lineNumber
468      * @param {number} columnNumber
469      * @return {WebInspector.DebuggerModel.Location}
470      */
471     uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber)
472     {
473         return this._scriptSnippetModel._uiLocationToRawLocation(uiSourceCode, lineNumber, columnNumber);
474     },
475
476     /**
477      * @param {string} sourceURL
478      * @return {string|null}
479      */
480     snippetIdForSourceURL: function(sourceURL)
481     {
482         return this._scriptSnippetModel._snippetIdForSourceURL(sourceURL);
483     },
484
485     /**
486      * @param {WebInspector.Script} script
487      */
488     addScript: function(script)
489     {
490         this._scriptSnippetModel._addScript(script);
491     }
492 }
493
494 /**
495  * @constructor
496  * @extends {WebInspector.StaticContentProvider}
497  * @param {WebInspector.Snippet} snippet
498  */
499 WebInspector.SnippetContentProvider = function(snippet)
500 {
501     WebInspector.StaticContentProvider.call(this, WebInspector.resourceTypes.Script, snippet.content);
502 }
503
504 WebInspector.SnippetContentProvider.prototype.__proto__ = WebInspector.StaticContentProvider.prototype;
505
506 /**
507  * @type {?WebInspector.ScriptSnippetModel}
508  */
509 WebInspector.scriptSnippetModel = null;