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.VBox} 34 */ 35 WebInspector.RevisionHistoryView = function() 36 { 37 WebInspector.VBox.call(this); 38 this.registerRequiredCSS("revisionHistory.css"); 39 this.element.classList.add("revision-history-drawer"); 40 this.element.classList.add("outline-disclosure"); 41 this._uiSourceCodeItems = new Map(); 42 43 var olElement = this.element.createChild("ol"); 44 this._treeOutline = new TreeOutline(olElement); 45 46 /** 47 * @param {!WebInspector.UISourceCode} uiSourceCode 48 * @this {WebInspector.RevisionHistoryView} 49 */ 50 function populateRevisions(uiSourceCode) 51 { 52 if (uiSourceCode.history.length) 53 this._createUISourceCodeItem(uiSourceCode); 54 } 55 56 WebInspector.workspace.uiSourceCodes().forEach(populateRevisions.bind(this)); 57 WebInspector.workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeContentCommitted, this._revisionAdded, this); 58 WebInspector.workspace.addEventListener(WebInspector.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); 59 WebInspector.workspace.addEventListener(WebInspector.Workspace.Events.ProjectRemoved, this._projectRemoved, this); 60 } 61 62 /** 63 * @param {!WebInspector.UISourceCode} uiSourceCode 64 */ 65 WebInspector.RevisionHistoryView.showHistory = function(uiSourceCode) 66 { 67 if (!WebInspector.RevisionHistoryView._view) 68 WebInspector.RevisionHistoryView._view = new WebInspector.RevisionHistoryView(); 69 var view = WebInspector.RevisionHistoryView._view; 70 WebInspector.inspectorView.showCloseableViewInDrawer("history", WebInspector.UIString("History"), view); 71 view._revealUISourceCode(uiSourceCode); 72 } 73 74 WebInspector.RevisionHistoryView.prototype = { 75 /** 76 * @param {!WebInspector.UISourceCode} uiSourceCode 77 */ 78 _createUISourceCodeItem: function(uiSourceCode) 79 { 80 var uiSourceCodeItem = new TreeElement(uiSourceCode.displayName(), null, true); 81 uiSourceCodeItem.selectable = false; 82 83 // Insert in sorted order 84 for (var i = 0; i < this._treeOutline.children.length; ++i) { 85 if (this._treeOutline.children[i].title.localeCompare(uiSourceCode.displayName()) > 0) { 86 this._treeOutline.insertChild(uiSourceCodeItem, i); 87 break; 88 } 89 } 90 if (i === this._treeOutline.children.length) 91 this._treeOutline.appendChild(uiSourceCodeItem); 92 93 this._uiSourceCodeItems.set(uiSourceCode, uiSourceCodeItem); 94 95 var revisionCount = uiSourceCode.history.length; 96 for (var i = revisionCount - 1; i >= 0; --i) { 97 var revision = uiSourceCode.history[i]; 98 var historyItem = new WebInspector.RevisionHistoryTreeElement(revision, uiSourceCode.history[i - 1], i !== revisionCount - 1); 99 uiSourceCodeItem.appendChild(historyItem); 100 } 101 102 var linkItem = new TreeElement("", null, false); 103 linkItem.selectable = false; 104 uiSourceCodeItem.appendChild(linkItem); 105 106 var revertToOriginal = linkItem.listItemElement.createChild("span", "revision-history-link revision-history-link-row"); 107 revertToOriginal.textContent = WebInspector.UIString("apply original content"); 108 revertToOriginal.addEventListener("click", this._revertToOriginal.bind(this, uiSourceCode)); 109 110 var clearHistoryElement = uiSourceCodeItem.listItemElement.createChild("span", "revision-history-link"); 111 clearHistoryElement.textContent = WebInspector.UIString("revert"); 112 clearHistoryElement.addEventListener("click", this._clearHistory.bind(this, uiSourceCode)); 113 return uiSourceCodeItem; 114 }, 115 116 /** 117 * @param {!WebInspector.UISourceCode} uiSourceCode 118 */ 119 _revertToOriginal: function(uiSourceCode) 120 { 121 uiSourceCode.revertToOriginal(); 122 123 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, { 124 action: WebInspector.UserMetrics.UserActionNames.ApplyOriginalContent, 125 url: uiSourceCode.url 126 }); 127 }, 128 129 /** 130 * @param {!WebInspector.UISourceCode} uiSourceCode 131 */ 132 _clearHistory: function(uiSourceCode) 133 { 134 uiSourceCode.revertAndClearHistory(this._removeUISourceCode.bind(this)); 135 136 WebInspector.notifications.dispatchEventToListeners(WebInspector.UserMetrics.UserAction, { 137 action: WebInspector.UserMetrics.UserActionNames.RevertRevision, 138 url: uiSourceCode.url 139 }); 140 }, 141 142 _revisionAdded: function(event) 143 { 144 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data.uiSourceCode); 145 var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode); 146 if (!uiSourceCodeItem) { 147 uiSourceCodeItem = this._createUISourceCodeItem(uiSourceCode); 148 return; 149 } 150 151 var historyLength = uiSourceCode.history.length; 152 var historyItem = new WebInspector.RevisionHistoryTreeElement(uiSourceCode.history[historyLength - 1], uiSourceCode.history[historyLength - 2], false); 153 if (uiSourceCodeItem.children.length) 154 uiSourceCodeItem.children[0].allowRevert(); 155 uiSourceCodeItem.insertChild(historyItem, 0); 156 }, 157 158 /** 159 * @param {!WebInspector.UISourceCode} uiSourceCode 160 */ 161 _revealUISourceCode: function(uiSourceCode) 162 { 163 var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode); 164 if (uiSourceCodeItem) { 165 uiSourceCodeItem.reveal(); 166 uiSourceCodeItem.expand(); 167 } 168 }, 169 170 _uiSourceCodeRemoved: function(event) 171 { 172 var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data); 173 this._removeUISourceCode(uiSourceCode); 174 }, 175 176 /** 177 * @param {!WebInspector.UISourceCode} uiSourceCode 178 */ 179 _removeUISourceCode: function(uiSourceCode) 180 { 181 var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode); 182 if (!uiSourceCodeItem) 183 return; 184 this._treeOutline.removeChild(uiSourceCodeItem); 185 this._uiSourceCodeItems.remove(uiSourceCode); 186 }, 187 188 _projectRemoved: function(event) 189 { 190 var project = event.data; 191 project.uiSourceCodes().forEach(this._removeUISourceCode.bind(this)); 192 }, 193 194 __proto__: WebInspector.VBox.prototype 195 } 196 197 /** 198 * @constructor 199 * @extends {TreeElement} 200 * @param {!WebInspector.Revision} revision 201 * @param {!WebInspector.Revision} baseRevision 202 * @param {boolean} allowRevert 203 */ 204 WebInspector.RevisionHistoryTreeElement = function(revision, baseRevision, allowRevert) 205 { 206 TreeElement.call(this, revision.timestamp.toLocaleTimeString(), null, true); 207 this.selectable = false; 208 209 this._revision = revision; 210 this._baseRevision = baseRevision; 211 212 this._revertElement = document.createElement("span"); 213 this._revertElement.className = "revision-history-link"; 214 this._revertElement.textContent = WebInspector.UIString("apply revision content"); 215 this._revertElement.addEventListener("click", this._revision.revertToThis.bind(this._revision), false); 216 if (!allowRevert) 217 this._revertElement.classList.add("hidden"); 218 } 219 220 WebInspector.RevisionHistoryTreeElement.prototype = { 221 onattach: function() 222 { 223 this.listItemElement.classList.add("revision-history-revision"); 224 }, 225 226 onexpand: function() 227 { 228 this.listItemElement.appendChild(this._revertElement); 229 230 if (this._wasExpandedOnce) 231 return; 232 this._wasExpandedOnce = true; 233 234 this.childrenListElement.classList.add("source-code"); 235 if (this._baseRevision) 236 this._baseRevision.requestContent(step1.bind(this)); 237 else 238 this._revision.uiSourceCode.requestOriginalContent(step1.bind(this)); 239 240 /** 241 * @param {?string} baseContent 242 * @this {WebInspector.RevisionHistoryTreeElement} 243 */ 244 function step1(baseContent) 245 { 246 this._revision.requestContent(step2.bind(this, baseContent)); 247 } 248 249 /** 250 * @param {?string} baseContent 251 * @param {?string} newContent 252 * @this {WebInspector.RevisionHistoryTreeElement} 253 */ 254 function step2(baseContent, newContent) 255 { 256 var baseLines = difflib.stringAsLines(baseContent); 257 var newLines = difflib.stringAsLines(newContent); 258 var sm = new difflib.SequenceMatcher(baseLines, newLines); 259 var opcodes = sm.get_opcodes(); 260 var lastWasSeparator = false; 261 262 for (var idx = 0; idx < opcodes.length; idx++) { 263 var code = opcodes[idx]; 264 var change = code[0]; 265 var b = code[1]; 266 var be = code[2]; 267 var n = code[3]; 268 var ne = code[4]; 269 var rowCount = Math.max(be - b, ne - n); 270 var topRows = []; 271 var bottomRows = []; 272 for (var i = 0; i < rowCount; i++) { 273 if (change === "delete" || (change === "replace" && b < be)) { 274 var lineNumber = b++; 275 this._createLine(lineNumber, null, baseLines[lineNumber], "removed"); 276 lastWasSeparator = false; 277 } 278 279 if (change === "insert" || (change === "replace" && n < ne)) { 280 var lineNumber = n++; 281 this._createLine(null, lineNumber, newLines[lineNumber], "added"); 282 lastWasSeparator = false; 283 } 284 285 if (change === "equal") { 286 b++; 287 n++; 288 if (!lastWasSeparator) 289 this._createLine(null, null, " \u2026", "separator"); 290 lastWasSeparator = true; 291 } 292 } 293 } 294 } 295 }, 296 297 oncollapse: function() 298 { 299 this._revertElement.remove(); 300 }, 301 302 /** 303 * @param {?number} baseLineNumber 304 * @param {?number} newLineNumber 305 * @param {string} lineContent 306 * @param {string} changeType 307 */ 308 _createLine: function(baseLineNumber, newLineNumber, lineContent, changeType) 309 { 310 var child = new TreeElement("", null, false); 311 child.selectable = false; 312 this.appendChild(child); 313 var lineElement = document.createElement("span"); 314 315 function appendLineNumber(lineNumber) 316 { 317 var numberString = lineNumber !== null ? numberToStringWithSpacesPadding(lineNumber + 1, 4) : spacesPadding(4); 318 var lineNumberSpan = document.createElement("span"); 319 lineNumberSpan.classList.add("webkit-line-number"); 320 lineNumberSpan.textContent = numberString; 321 child.listItemElement.appendChild(lineNumberSpan); 322 } 323 324 appendLineNumber(baseLineNumber); 325 appendLineNumber(newLineNumber); 326 327 var contentSpan = document.createElement("span"); 328 contentSpan.textContent = lineContent; 329 child.listItemElement.appendChild(contentSpan); 330 child.listItemElement.classList.add("revision-history-line"); 331 contentSpan.classList.add("revision-history-line-" + changeType); 332 }, 333 334 allowRevert: function() 335 { 336 this._revertElement.classList.remove("hidden"); 337 }, 338 339 __proto__: TreeElement.prototype 340 } 341