/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * @constructor * @extends {WebInspector.Object} * @param {!WebInspector.Setting} breakpointStorage * @param {!WebInspector.DebuggerModel} debuggerModel * @param {!WebInspector.Workspace} workspace */ WebInspector.BreakpointManager = function(breakpointStorage, debuggerModel, workspace) { this._storage = new WebInspector.BreakpointManager.Storage(this, breakpointStorage); this._debuggerModel = debuggerModel; this._workspace = workspace; this._breakpointForDebuggerId = {}; this._breakpointsForUISourceCode = new Map(); this._breakpointsForPrimaryUISourceCode = new Map(); this._sourceFilesWithRestoredBreakpoints = {}; this._debuggerModel.addEventListener(WebInspector.DebuggerModel.Events.BreakpointResolved, this._breakpointResolved, this); this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectWillReset, this._projectWillReset, this); this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this); this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); } WebInspector.BreakpointManager.Events = { BreakpointAdded: "breakpoint-added", BreakpointRemoved: "breakpoint-removed" } WebInspector.BreakpointManager._sourceFileId = function(uiSourceCode) { if (!uiSourceCode.url) return ""; var deobfuscatedPrefix = uiSourceCode.formatted() ? "deobfuscated:" : ""; return deobfuscatedPrefix + uiSourceCode.uri(); } /** * @param {string} sourceFileId * @param {number} lineNumber * @return {string} */ WebInspector.BreakpointManager._breakpointStorageId = function(sourceFileId, lineNumber) { if (!sourceFileId) return ""; return sourceFileId + ":" + lineNumber; } WebInspector.BreakpointManager.prototype = { /** * @param {string} sourceFileId */ _provisionalBreakpointsForSourceFileId: function(sourceFileId) { var result = new StringMap(); for (var debuggerId in this._breakpointForDebuggerId) { var breakpoint = this._breakpointForDebuggerId[debuggerId]; if (breakpoint._sourceFileId === sourceFileId) result.put(breakpoint._breakpointStorageId(), breakpoint); } return result; }, /** * @param {!WebInspector.UISourceCode} uiSourceCode */ _restoreBreakpoints: function(uiSourceCode) { var sourceFileId = WebInspector.BreakpointManager._sourceFileId(uiSourceCode); if (!sourceFileId || this._sourceFilesWithRestoredBreakpoints[sourceFileId]) return; this._sourceFilesWithRestoredBreakpoints[sourceFileId] = true; this._storage.mute(); var breakpointItems = this._storage.breakpointItems(uiSourceCode); var provisionalBreakpoints = this._provisionalBreakpointsForSourceFileId(sourceFileId); for (var i = 0; i < breakpointItems.length; ++i) { var breakpointItem = breakpointItems[i]; var itemStorageId = WebInspector.BreakpointManager._breakpointStorageId(breakpointItem.sourceFileId, breakpointItem.lineNumber); var provisionalBreakpoint = provisionalBreakpoints.get(itemStorageId); if (provisionalBreakpoint) { if (!this._breakpointsForPrimaryUISourceCode.get(uiSourceCode)) this._breakpointsForPrimaryUISourceCode.put(uiSourceCode, []); this._breakpointsForPrimaryUISourceCode.get(uiSourceCode).push(provisionalBreakpoint); provisionalBreakpoint._updateInDebugger(); } else { this._innerSetBreakpoint(uiSourceCode, breakpointItem.lineNumber, breakpointItem.condition, breakpointItem.enabled); } } this._storage.unmute(); }, /** * @param {!WebInspector.Event} event */ _uiSourceCodeAdded: function(event) { var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); this._restoreBreakpoints(uiSourceCode); if (uiSourceCode.contentType() === WebInspector.resourceTypes.Script || uiSourceCode.contentType() === WebInspector.resourceTypes.Document) { uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.SourceMappingChanged, this._uiSourceCodeMappingChanged, this); uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.FormattedChanged, this._uiSourceCodeFormatted, this); } }, /** * @param {!WebInspector.Event} event */ _uiSourceCodeFormatted: function(event) { var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target); this._restoreBreakpoints(uiSourceCode); }, /** * @param {!WebInspector.Event} event */ _uiSourceCodeRemoved: function(event) { var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); this._removeUISourceCode(uiSourceCode); }, /** * @param {!WebInspector.Event} event */ _uiSourceCodeMappingChanged: function(event) { var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.target); var breakpoints = this._breakpointsForPrimaryUISourceCode.get(uiSourceCode) || []; for (var i = 0; i < breakpoints.length; ++i) breakpoints[i]._updateInDebugger(); }, /** * @param {!WebInspector.UISourceCode} uiSourceCode */ _removeUISourceCode: function(uiSourceCode) { var breakpoints = this._breakpointsForPrimaryUISourceCode.get(uiSourceCode) || []; for (var i = 0; i < breakpoints.length; ++i) breakpoints[i]._resetLocations(); var sourceFileId = WebInspector.BreakpointManager._sourceFileId(uiSourceCode); delete this._sourceFilesWithRestoredBreakpoints[sourceFileId]; uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.FormattedChanged, this._uiSourceCodeFormatted, this); uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.SourceMappingChanged, this._uiSourceCodeMappingChanged, this); this._breakpointsForPrimaryUISourceCode.remove(uiSourceCode); }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @param {number} lineNumber * @param {string} condition * @param {boolean} enabled * @return {!WebInspector.BreakpointManager.Breakpoint} */ setBreakpoint: function(uiSourceCode, lineNumber, condition, enabled) { this._debuggerModel.setBreakpointsActive(true); return this._innerSetBreakpoint(uiSourceCode, lineNumber, condition, enabled); }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @param {number} lineNumber * @param {string} condition * @param {boolean} enabled * @return {!WebInspector.BreakpointManager.Breakpoint} */ _innerSetBreakpoint: function(uiSourceCode, lineNumber, condition, enabled) { var breakpoint = this.findBreakpoint(uiSourceCode, lineNumber); if (breakpoint) { breakpoint._updateBreakpoint(condition, enabled); return breakpoint; } var projectId = uiSourceCode.project().id(); var path = uiSourceCode.path(); var sourceFileId = WebInspector.BreakpointManager._sourceFileId(uiSourceCode); breakpoint = new WebInspector.BreakpointManager.Breakpoint(this, projectId, path, sourceFileId, lineNumber, condition, enabled); if (!this._breakpointsForPrimaryUISourceCode.get(uiSourceCode)) this._breakpointsForPrimaryUISourceCode.put(uiSourceCode, []); this._breakpointsForPrimaryUISourceCode.get(uiSourceCode).push(breakpoint); return breakpoint; }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @param {number} lineNumber * @return {?WebInspector.BreakpointManager.Breakpoint} */ findBreakpoint: function(uiSourceCode, lineNumber) { var breakpoints = this._breakpointsForUISourceCode.get(uiSourceCode); var lineBreakpoints = breakpoints ? breakpoints[lineNumber] : null; return lineBreakpoints ? lineBreakpoints[0] : null; }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @return {!Array.} */ breakpointsForUISourceCode: function(uiSourceCode) { var breakpoints = this._breakpointsForUISourceCode.get(uiSourceCode); var allLineBreakpoints = breakpoints ? Object.values(breakpoints) : []; var result = []; for (var i = 0; i < allLineBreakpoints.length; ++i) result = result.concat(allLineBreakpoints[i]); return result; }, /** * @return {!Array.} */ allBreakpoints: function() { var result = []; var uiSourceCodes = this._breakpointsForUISourceCode.keys(); for (var i = 0; i < uiSourceCodes.length; ++i) result = result.concat(this.breakpointsForUISourceCode(uiSourceCodes[i])); return result; }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @return {!Array.<{breakpoint: !WebInspector.BreakpointManager.Breakpoint, uiLocation: !WebInspector.UILocation}>} */ breakpointLocationsForUISourceCode: function(uiSourceCode) { var breakpoints = this._breakpointsForUISourceCode.get(uiSourceCode); var breakpointLines = breakpoints ? Object.keys(breakpoints) : []; var result = []; for (var i = 0; i < breakpointLines.length; ++i) { var lineNumber = parseInt(breakpointLines[i], 10); if (isNaN(lineNumber)) continue; var lineBreakpoints = breakpoints[lineNumber]; for (var j = 0; j < lineBreakpoints.length; ++j) { var breakpoint = lineBreakpoints[j]; var uiLocation = new WebInspector.UILocation(uiSourceCode, lineNumber, 0); result.push({breakpoint: breakpoint, uiLocation: uiLocation}); } } return result; }, /** * @return {!Array.<{breakpoint: !WebInspector.BreakpointManager.Breakpoint, uiLocation: !WebInspector.UILocation}>} */ allBreakpointLocations: function() { var result = []; var uiSourceCodes = this._breakpointsForUISourceCode.keys(); for (var i = 0; i < uiSourceCodes.length; ++i) result = result.concat(this.breakpointLocationsForUISourceCode(uiSourceCodes[i])); return result; }, /** * @param {boolean} toggleState */ toggleAllBreakpoints: function(toggleState) { var breakpoints = this.allBreakpoints(); for (var i = 0; i < breakpoints.length; ++i) breakpoints[i].setEnabled(toggleState); }, removeAllBreakpoints: function() { var breakpoints = this.allBreakpoints(); for (var i = 0; i < breakpoints.length; ++i) breakpoints[i].remove(); }, removeProvisionalBreakpoints: function() { for (var debuggerId in this._breakpointForDebuggerId) this._debuggerModel.removeBreakpoint(debuggerId); }, _projectWillReset: function(event) { var project = /** @type {!WebInspector.Project} */ (event.data); var uiSourceCodes = project.uiSourceCodes(); for (var i = 0; i < uiSourceCodes.length; ++i) this._removeUISourceCode(uiSourceCodes[i]); }, _breakpointResolved: function(event) { var breakpointId = /** @type {!DebuggerAgent.BreakpointId} */ (event.data.breakpointId); var location = /** @type {!WebInspector.DebuggerModel.Location} */ (event.data.location); var breakpoint = this._breakpointForDebuggerId[breakpointId]; if (!breakpoint) return; breakpoint._addResolvedLocation(location); }, /** * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint * @param {boolean} removeFromStorage */ _removeBreakpoint: function(breakpoint, removeFromStorage) { var uiSourceCode = breakpoint.uiSourceCode(); var breakpoints = uiSourceCode ? this._breakpointsForPrimaryUISourceCode.get(uiSourceCode) || [] : []; var index = breakpoints.indexOf(breakpoint); if (index > -1) breakpoints.splice(index, 1); console.assert(!breakpoint._debuggerId) if (removeFromStorage) this._storage._removeBreakpoint(breakpoint); }, /** * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint * @param {!WebInspector.UILocation} uiLocation */ _uiLocationAdded: function(breakpoint, uiLocation) { var breakpoints = this._breakpointsForUISourceCode.get(uiLocation.uiSourceCode); if (!breakpoints) { breakpoints = {}; this._breakpointsForUISourceCode.put(uiLocation.uiSourceCode, breakpoints); } var lineBreakpoints = breakpoints[uiLocation.lineNumber]; if (!lineBreakpoints) { lineBreakpoints = []; breakpoints[uiLocation.lineNumber] = lineBreakpoints; } lineBreakpoints.push(breakpoint); this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.BreakpointAdded, {breakpoint: breakpoint, uiLocation: uiLocation}); }, /** * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint * @param {!WebInspector.UILocation} uiLocation */ _uiLocationRemoved: function(breakpoint, uiLocation) { var breakpoints = this._breakpointsForUISourceCode.get(uiLocation.uiSourceCode); if (!breakpoints) return; var lineBreakpoints = breakpoints[uiLocation.lineNumber]; if (!lineBreakpoints) return; lineBreakpoints.remove(breakpoint); if (!lineBreakpoints.length) delete breakpoints[uiLocation.lineNumber]; if (Object.keys(breakpoints).length === 0) this._breakpointsForUISourceCode.remove(uiLocation.uiSourceCode); this.dispatchEventToListeners(WebInspector.BreakpointManager.Events.BreakpointRemoved, {breakpoint: breakpoint, uiLocation: uiLocation}); }, __proto__: WebInspector.Object.prototype } /** * @constructor * @param {!WebInspector.BreakpointManager} breakpointManager * @param {string} projectId * @param {string} path * @param {string} sourceFileId * @param {number} lineNumber * @param {string} condition * @param {boolean} enabled */ WebInspector.BreakpointManager.Breakpoint = function(breakpointManager, projectId, path, sourceFileId, lineNumber, condition, enabled) { this._breakpointManager = breakpointManager; this._projectId = projectId; this._path = path; this._lineNumber = lineNumber; this._sourceFileId = sourceFileId; /** @type {!Array.} */ this._liveLocations = []; /** @type {!Object.} */ this._uiLocations = {}; // Force breakpoint update. /** @type {string} */ this._condition; /** @type {boolean} */ this._enabled; this._updateBreakpoint(condition, enabled); } WebInspector.BreakpointManager.Breakpoint.prototype = { /** * @return {string} */ projectId: function() { return this._projectId; }, /** * @return {string} */ path: function() { return this._path; }, /** * @return {number} */ lineNumber: function() { return this._lineNumber; }, /** * @return {?WebInspector.UISourceCode} */ uiSourceCode: function() { return this._breakpointManager._workspace.uiSourceCode(this._projectId, this._path); }, /** * @param {!WebInspector.DebuggerModel.Location} location */ _addResolvedLocation: function(location) { this._liveLocations.push(this._breakpointManager._debuggerModel.createLiveLocation(location, this._locationUpdated.bind(this, location))); }, /** * @param {!WebInspector.DebuggerModel.Location} location * @param {!WebInspector.UILocation} uiLocation */ _locationUpdated: function(location, uiLocation) { var stringifiedLocation = location.scriptId + ":" + location.lineNumber + ":" + location.columnNumber; var oldUILocation = /** @type {!WebInspector.UILocation} */ (this._uiLocations[stringifiedLocation]); if (oldUILocation) this._breakpointManager._uiLocationRemoved(this, oldUILocation); if (this._uiLocations[""]) { var defaultLocation = this._uiLocations[""]; delete this._uiLocations[""]; this._breakpointManager._uiLocationRemoved(this, defaultLocation); } this._uiLocations[stringifiedLocation] = uiLocation; this._breakpointManager._uiLocationAdded(this, uiLocation); }, /** * @return {boolean} */ enabled: function() { return this._enabled; }, /** * @param {boolean} enabled */ setEnabled: function(enabled) { this._updateBreakpoint(this._condition, enabled); }, /** * @return {string} */ condition: function() { return this._condition; }, /** * @param {string} condition */ setCondition: function(condition) { this._updateBreakpoint(condition, this._enabled); }, /** * @param {string} condition * @param {boolean} enabled */ _updateBreakpoint: function(condition, enabled) { if (this._enabled === enabled && this._condition === condition) return; this._removeFromDebugger(); this._enabled = enabled; this._condition = condition; this._breakpointManager._storage._updateBreakpoint(this); this._fakeBreakpointAtPrimaryLocation(); this._updateInDebugger(); }, _updateInDebugger: function() { var uiSourceCode = this.uiSourceCode(); if (!uiSourceCode || !uiSourceCode.hasSourceMapping()) return; var scriptFile = uiSourceCode && uiSourceCode.scriptFile(); if (this._enabled && !(scriptFile && scriptFile.hasDivergedFromVM())) this._setInDebugger(); }, /** * @param {boolean=} keepInStorage */ remove: function(keepInStorage) { var removeFromStorage = !keepInStorage; this._resetLocations(); this._removeFromDebugger(); this._breakpointManager._removeBreakpoint(this, removeFromStorage); }, _setInDebugger: function() { this._removeFromDebugger(); var uiSourceCode = this._breakpointManager._workspace.uiSourceCode(this._projectId, this._path); if (!uiSourceCode) return; var rawLocation = uiSourceCode.uiLocationToRawLocation(this._lineNumber, 0); var debuggerModelLocation = /** @type {!WebInspector.DebuggerModel.Location} */ (rawLocation); if (debuggerModelLocation) this._breakpointManager._debuggerModel.setBreakpointByScriptLocation(debuggerModelLocation, this._condition, this._didSetBreakpointInDebugger.bind(this)); else if (uiSourceCode.url) this._breakpointManager._debuggerModel.setBreakpointByURL(uiSourceCode.url, this._lineNumber, 0, this._condition, this._didSetBreakpointInDebugger.bind(this)); }, /** * @this {WebInspector.BreakpointManager.Breakpoint} * @param {?DebuggerAgent.BreakpointId} breakpointId * @param {!Array.} locations */ _didSetBreakpointInDebugger: function(breakpointId, locations) { if (!breakpointId) { this._resetLocations(); this._breakpointManager._removeBreakpoint(this, false); return; } this._debuggerId = breakpointId; this._breakpointManager._breakpointForDebuggerId[breakpointId] = this; if (!locations.length) { this._fakeBreakpointAtPrimaryLocation(); return; } this._resetLocations(); for (var i = 0; i < locations.length; ++i) { var script = this._breakpointManager._debuggerModel.scriptForId(locations[i].scriptId); var uiLocation = script.rawLocationToUILocation(locations[i].lineNumber, locations[i].columnNumber); if (this._breakpointManager.findBreakpoint(uiLocation.uiSourceCode, uiLocation.lineNumber)) { // location clash this.remove(); return; } } for (var i = 0; i < locations.length; ++i) this._addResolvedLocation(locations[i]); }, _removeFromDebugger: function() { if (!this._debuggerId) return; this._breakpointManager._debuggerModel.removeBreakpoint(this._debuggerId); delete this._breakpointManager._breakpointForDebuggerId[this._debuggerId]; delete this._debuggerId; }, _resetLocations: function() { for (var stringifiedLocation in this._uiLocations) this._breakpointManager._uiLocationRemoved(this, this._uiLocations[stringifiedLocation]); for (var i = 0; i < this._liveLocations.length; ++i) this._liveLocations[i].dispose(); this._liveLocations = []; this._uiLocations = {}; }, /** * @return {string} */ _breakpointStorageId: function() { return WebInspector.BreakpointManager._breakpointStorageId(this._sourceFileId, this._lineNumber); }, _fakeBreakpointAtPrimaryLocation: function() { this._resetLocations(); var uiSourceCode = this._breakpointManager._workspace.uiSourceCode(this._projectId, this._path); if (!uiSourceCode) return; var uiLocation = new WebInspector.UILocation(uiSourceCode, this._lineNumber, 0); this._uiLocations[""] = uiLocation; this._breakpointManager._uiLocationAdded(this, uiLocation); } } /** * @constructor * @param {!WebInspector.BreakpointManager} breakpointManager * @param {!WebInspector.Setting} setting */ WebInspector.BreakpointManager.Storage = function(breakpointManager, setting) { this._breakpointManager = breakpointManager; this._setting = setting; var breakpoints = this._setting.get(); /** @type {!Object.} */ this._breakpoints = {}; for (var i = 0; i < breakpoints.length; ++i) { var breakpoint = /** @type {!WebInspector.BreakpointManager.Storage.Item} */ (breakpoints[i]); this._breakpoints[breakpoint.sourceFileId + ":" + breakpoint.lineNumber] = breakpoint; } } WebInspector.BreakpointManager.Storage.prototype = { mute: function() { this._muted = true; }, unmute: function() { delete this._muted; }, /** * @param {!WebInspector.UISourceCode} uiSourceCode * @return {!Array.} */ breakpointItems: function(uiSourceCode) { var result = []; var sourceFileId = WebInspector.BreakpointManager._sourceFileId(uiSourceCode); for (var id in this._breakpoints) { var breakpoint = this._breakpoints[id]; if (breakpoint.sourceFileId === sourceFileId) result.push(breakpoint); } return result; }, /** * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint */ _updateBreakpoint: function(breakpoint) { if (this._muted || !breakpoint._breakpointStorageId()) return; this._breakpoints[breakpoint._breakpointStorageId()] = new WebInspector.BreakpointManager.Storage.Item(breakpoint); this._save(); }, /** * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint */ _removeBreakpoint: function(breakpoint) { if (this._muted) return; delete this._breakpoints[breakpoint._breakpointStorageId()]; this._save(); }, _save: function() { var breakpointsArray = []; for (var id in this._breakpoints) breakpointsArray.push(this._breakpoints[id]); this._setting.set(breakpointsArray); } } /** * @constructor * @param {!WebInspector.BreakpointManager.Breakpoint} breakpoint */ WebInspector.BreakpointManager.Storage.Item = function(breakpoint) { this.sourceFileId = breakpoint._sourceFileId; this.lineNumber = breakpoint.lineNumber(); this.condition = breakpoint.condition(); this.enabled = breakpoint.enabled(); } /** @type {!WebInspector.BreakpointManager} */ WebInspector.breakpointManager;