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