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 * @implements {WebInspector.SourceMapping} 34 * @param {!WebInspector.CSSStyleModel} cssModel 35 * @param {!WebInspector.Workspace} workspace 36 */ 37 WebInspector.StylesSourceMapping = function(cssModel, workspace) 38 { 39 this._cssModel = cssModel; 40 this._workspace = workspace; 41 this._workspace.addEventListener(WebInspector.Workspace.Events.ProjectWillReset, this._projectWillReset, this); 42 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAddedToWorkspace, this); 43 this._workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); 44 45 WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.MainFrameCreatedOrNavigated, this._mainFrameCreatedOrNavigated, this); 46 47 this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetChanged, this); 48 this._initialize(); 49 } 50 51 WebInspector.StylesSourceMapping.MinorChangeUpdateTimeoutMs = 1000; 52 53 WebInspector.StylesSourceMapping.prototype = { 54 /** 55 * @param {!WebInspector.RawLocation} rawLocation 56 * @return {?WebInspector.UILocation} 57 */ 58 rawLocationToUILocation: function(rawLocation) 59 { 60 var location = /** @type WebInspector.CSSLocation */ (rawLocation); 61 var uiSourceCode = this._workspace.uiSourceCodeForURL(location.url); 62 if (!uiSourceCode) 63 return null; 64 return new WebInspector.UILocation(uiSourceCode, location.lineNumber, location.columnNumber); 65 }, 66 67 /** 68 * @param {!WebInspector.UISourceCode} uiSourceCode 69 * @param {number} lineNumber 70 * @param {number} columnNumber 71 * @return {!WebInspector.RawLocation} 72 */ 73 uiLocationToRawLocation: function(uiSourceCode, lineNumber, columnNumber) 74 { 75 return new WebInspector.CSSLocation(uiSourceCode.url || "", lineNumber, columnNumber); 76 }, 77 78 /** 79 * @param {!WebInspector.CSSStyleSheetHeader} header 80 */ 81 addHeader: function(header) 82 { 83 var url = header.resourceURL(); 84 if (!url) 85 return; 86 87 header.pushSourceMapping(this); 88 var map = this._urlToHeadersByFrameId[url]; 89 if (!map) { 90 map = /** @type {!StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>} */ (new StringMap()); 91 this._urlToHeadersByFrameId[url] = map; 92 } 93 var headersById = map.get(header.frameId); 94 if (!headersById) { 95 headersById = /** @type {!StringMap.<!WebInspector.CSSStyleSheetHeader>} */ (new StringMap()); 96 map.put(header.frameId, headersById); 97 } 98 headersById.put(header.id, header); 99 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 100 if (uiSourceCode) 101 this._bindUISourceCode(uiSourceCode, header); 102 }, 103 104 /** 105 * @param {!WebInspector.CSSStyleSheetHeader} header 106 */ 107 removeHeader: function(header) 108 { 109 var url = header.resourceURL(); 110 if (!url) 111 return; 112 113 var map = this._urlToHeadersByFrameId[url]; 114 console.assert(map); 115 var headersById = map.get(header.frameId); 116 console.assert(headersById); 117 headersById.remove(header.id); 118 119 if (!headersById.size()) { 120 map.remove(header.frameId); 121 if (!map.size()) { 122 delete this._urlToHeadersByFrameId[url]; 123 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 124 if (uiSourceCode) 125 this._unbindUISourceCode(uiSourceCode); 126 } 127 } 128 }, 129 130 /** 131 * @param {!WebInspector.UISourceCode} uiSourceCode 132 */ 133 _unbindUISourceCode: function(uiSourceCode) 134 { 135 var styleFile = this._styleFiles.get(uiSourceCode); 136 if (!styleFile) 137 return; 138 styleFile.dispose(); 139 this._styleFiles.remove(uiSourceCode); 140 }, 141 142 /** 143 * @param {!WebInspector.Event} event 144 */ 145 _uiSourceCodeAddedToWorkspace: function(event) 146 { 147 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); 148 var url = uiSourceCode.url; 149 if (!url || !this._urlToHeadersByFrameId[url]) 150 return; 151 this._bindUISourceCode(uiSourceCode, this._urlToHeadersByFrameId[url].values()[0].values()[0]); 152 }, 153 154 /** 155 * @param {!WebInspector.UISourceCode} uiSourceCode 156 * @param {!WebInspector.CSSStyleSheetHeader} header 157 */ 158 _bindUISourceCode: function(uiSourceCode, header) 159 { 160 if (this._styleFiles.get(uiSourceCode) || header.isInline) 161 return; 162 var url = uiSourceCode.url; 163 this._styleFiles.put(uiSourceCode, new WebInspector.StyleFile(uiSourceCode, this)); 164 header.updateLocations(); 165 }, 166 167 /** 168 * @param {!WebInspector.Event} event 169 */ 170 _projectWillReset: function(event) 171 { 172 var project = /** @type {!WebInspector.Project} */ (event.data); 173 var uiSourceCodes = project.uiSourceCodes(); 174 for (var i = 0; i < uiSourceCodes.length; ++i) 175 this._unbindUISourceCode(uiSourceCodes[i]); 176 }, 177 178 /** 179 * @param {!WebInspector.Event} event 180 */ 181 _uiSourceCodeRemoved: function(event) 182 { 183 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); 184 this._unbindUISourceCode(uiSourceCode); 185 }, 186 187 _initialize: function() 188 { 189 /** @type {!Object.<string, !StringMap.<!StringMap.<!WebInspector.CSSStyleSheetHeader>>>} */ 190 this._urlToHeadersByFrameId = {}; 191 /** @type {!Map.<!WebInspector.UISourceCode, !WebInspector.StyleFile>} */ 192 this._styleFiles = new Map(); 193 }, 194 195 /** 196 * @param {!WebInspector.Event} event 197 */ 198 _mainFrameCreatedOrNavigated: function(event) 199 { 200 for (var url in this._urlToHeadersByFrameId) { 201 var uiSourceCode = this._workspace.uiSourceCodeForURL(url); 202 if (!uiSourceCode) 203 continue; 204 this._unbindUISourceCode(uiSourceCode); 205 } 206 this._initialize(); 207 }, 208 209 /** 210 * @param {!WebInspector.UISourceCode} uiSourceCode 211 * @param {string} content 212 * @param {boolean} majorChange 213 * @param {function(?string)} userCallback 214 */ 215 _setStyleContent: function(uiSourceCode, content, majorChange, userCallback) 216 { 217 var styleSheetIds = this._cssModel.styleSheetIdsForURL(uiSourceCode.url); 218 if (!styleSheetIds.length) { 219 userCallback("No stylesheet found: " + uiSourceCode.url); 220 return; 221 } 222 223 this._isSettingContent = true; 224 225 /** 226 * @param {?Protocol.Error} error 227 * @this {WebInspector.StylesSourceMapping} 228 */ 229 function callback(error) 230 { 231 userCallback(error); 232 delete this._isSettingContent; 233 } 234 this._cssModel.setStyleSheetText(styleSheetIds[0], content, majorChange, callback.bind(this)); 235 }, 236 237 /** 238 * @param {!WebInspector.Event} event 239 */ 240 _styleSheetChanged: function(event) 241 { 242 if (this._isSettingContent) 243 return; 244 245 if (event.data.majorChange) { 246 this._updateStyleSheetText(event.data.styleSheetId); 247 return; 248 } 249 250 this._updateStyleSheetTextSoon(event.data.styleSheetId); 251 }, 252 253 /** 254 * @param {!CSSAgent.StyleSheetId} styleSheetId 255 */ 256 _updateStyleSheetTextSoon: function(styleSheetId) 257 { 258 if (this._updateStyleSheetTextTimer) 259 clearTimeout(this._updateStyleSheetTextTimer); 260 261 this._updateStyleSheetTextTimer = setTimeout(this._updateStyleSheetText.bind(this, styleSheetId), WebInspector.StylesSourceMapping.MinorChangeUpdateTimeoutMs); 262 }, 263 264 /** 265 * @param {!CSSAgent.StyleSheetId} styleSheetId 266 */ 267 _updateStyleSheetText: function(styleSheetId) 268 { 269 if (this._updateStyleSheetTextTimer) { 270 clearTimeout(this._updateStyleSheetTextTimer); 271 delete this._updateStyleSheetTextTimer; 272 } 273 274 CSSAgent.getStyleSheetText(styleSheetId, callback.bind(this)); 275 276 /** 277 * @param {?string} error 278 * @param {string} content 279 * @this {WebInspector.StylesSourceMapping} 280 */ 281 function callback(error, content) 282 { 283 if (!error) 284 this._innerStyleSheetChanged(styleSheetId, content); 285 } 286 }, 287 288 /** 289 * @param {!CSSAgent.StyleSheetId} styleSheetId 290 * @param {string} content 291 */ 292 _innerStyleSheetChanged: function(styleSheetId, content) 293 { 294 var header = this._cssModel.styleSheetHeaderForId(styleSheetId); 295 if (!header) 296 return; 297 var styleSheetURL = header.resourceURL(); 298 if (!styleSheetURL) 299 return; 300 301 var uiSourceCode = this._workspace.uiSourceCodeForURL(styleSheetURL) 302 if (!uiSourceCode) 303 return; 304 305 var styleFile = this._styleFiles.get(uiSourceCode); 306 if (styleFile) 307 styleFile.addRevision(content); 308 } 309 } 310 311 /** 312 * @constructor 313 * @param {!WebInspector.UISourceCode} uiSourceCode 314 * @param {!WebInspector.StylesSourceMapping} mapping 315 */ 316 WebInspector.StyleFile = function(uiSourceCode, mapping) 317 { 318 this._uiSourceCode = uiSourceCode; 319 this._mapping = mapping; 320 this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); 321 this._uiSourceCode.addEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); 322 } 323 324 WebInspector.StyleFile.updateTimeout = 200; 325 326 WebInspector.StyleFile.sourceURLRegex = /\n[\040\t]*\/\*#[\040\t]sourceURL=[\040\t]*([^\s]*)[\040\t]*\*\/[\040\t]*$/m; 327 328 WebInspector.StyleFile.prototype = { 329 _workingCopyCommitted: function(event) 330 { 331 if (this._isAddingRevision) 332 return; 333 334 this._commitIncrementalEdit(true); 335 }, 336 337 _workingCopyChanged: function(event) 338 { 339 if (this._isAddingRevision) 340 return; 341 342 // FIXME: Extensions tests override updateTimeout because extensions don't have any control over applying changes to domain specific bindings. 343 if (WebInspector.StyleFile.updateTimeout >= 0) { 344 this._incrementalUpdateTimer = setTimeout(this._commitIncrementalEdit.bind(this, false), WebInspector.StyleFile.updateTimeout) 345 } else 346 this._commitIncrementalEdit(false); 347 }, 348 349 /** 350 * @param {boolean} majorChange 351 */ 352 _commitIncrementalEdit: function(majorChange) 353 { 354 this._clearIncrementalUpdateTimer(); 355 this._mapping._setStyleContent(this._uiSourceCode, this._uiSourceCode.workingCopy(), majorChange, this._styleContentSet.bind(this)); 356 }, 357 358 /** 359 * @param {?string} error 360 */ 361 _styleContentSet: function(error) 362 { 363 if (error) 364 WebInspector.showErrorMessage(error); 365 }, 366 367 _clearIncrementalUpdateTimer: function() 368 { 369 if (!this._incrementalUpdateTimer) 370 return; 371 clearTimeout(this._incrementalUpdateTimer); 372 delete this._incrementalUpdateTimer; 373 }, 374 375 /** 376 * @param {string} content 377 */ 378 addRevision: function(content) 379 { 380 this._isAddingRevision = true; 381 if (this._uiSourceCode.project().type() === WebInspector.projectTypes.FileSystem) 382 content = content.replace(WebInspector.StyleFile.sourceURLRegex, ""); 383 this._uiSourceCode.addRevision(content); 384 delete this._isAddingRevision; 385 }, 386 387 dispose: function() 388 { 389 this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyCommitted, this._workingCopyCommitted, this); 390 this._uiSourceCode.removeEventListener(WebInspector.UISourceCode.Events.WorkingCopyChanged, this._workingCopyChanged, this); 391 } 392 } 393