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 */
35WebInspector.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 */
65WebInspector.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
74WebInspector.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.put(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", uiSourceCode.revertToOriginal.bind(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    _clearHistory: function(uiSourceCode)
120    {
121        uiSourceCode.revertAndClearHistory(this._removeUISourceCode.bind(this));
122    },
123
124    _revisionAdded: function(event)
125    {
126        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data.uiSourceCode);
127        var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode);
128        if (!uiSourceCodeItem) {
129            uiSourceCodeItem = this._createUISourceCodeItem(uiSourceCode);
130            return;
131        }
132
133        var historyLength = uiSourceCode.history.length;
134        var historyItem = new WebInspector.RevisionHistoryTreeElement(uiSourceCode.history[historyLength - 1], uiSourceCode.history[historyLength - 2], false);
135        if (uiSourceCodeItem.children.length)
136            uiSourceCodeItem.children[0].allowRevert();
137        uiSourceCodeItem.insertChild(historyItem, 0);
138    },
139
140    /**
141     * @param {!WebInspector.UISourceCode} uiSourceCode
142     */
143    _revealUISourceCode: function(uiSourceCode)
144    {
145        var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode);
146        if (uiSourceCodeItem) {
147            uiSourceCodeItem.reveal();
148            uiSourceCodeItem.expand();
149        }
150    },
151
152    _uiSourceCodeRemoved: function(event)
153    {
154        var uiSourceCode = /** @type {!WebInspector.UISourceCode} */ (event.data);
155        this._removeUISourceCode(uiSourceCode);
156    },
157
158    /**
159     * @param {!WebInspector.UISourceCode} uiSourceCode
160     */
161    _removeUISourceCode: function(uiSourceCode)
162    {
163        var uiSourceCodeItem = this._uiSourceCodeItems.get(uiSourceCode);
164        if (!uiSourceCodeItem)
165            return;
166        this._treeOutline.removeChild(uiSourceCodeItem);
167        this._uiSourceCodeItems.remove(uiSourceCode);
168    },
169
170    _projectRemoved: function(event)
171    {
172        var project = event.data;
173        project.uiSourceCodes().forEach(this._removeUISourceCode.bind(this));
174    },
175
176    __proto__: WebInspector.VBox.prototype
177}
178
179/**
180 * @constructor
181 * @extends {TreeElement}
182 * @param {!WebInspector.Revision} revision
183 * @param {!WebInspector.Revision} baseRevision
184 * @param {boolean} allowRevert
185 */
186WebInspector.RevisionHistoryTreeElement = function(revision, baseRevision, allowRevert)
187{
188    TreeElement.call(this, revision.timestamp.toLocaleTimeString(), null, true);
189    this.selectable = false;
190
191    this._revision = revision;
192    this._baseRevision = baseRevision;
193
194    this._revertElement = document.createElement("span");
195    this._revertElement.className = "revision-history-link";
196    this._revertElement.textContent = WebInspector.UIString("apply revision content");
197    this._revertElement.addEventListener("click", this._revision.revertToThis.bind(this._revision), false);
198    if (!allowRevert)
199        this._revertElement.classList.add("hidden");
200}
201
202WebInspector.RevisionHistoryTreeElement.prototype = {
203    onattach: function()
204    {
205        this.listItemElement.classList.add("revision-history-revision");
206    },
207
208    onexpand: function()
209    {
210        this.listItemElement.appendChild(this._revertElement);
211
212        if (this._wasExpandedOnce)
213            return;
214        this._wasExpandedOnce = true;
215
216        this.childrenListElement.classList.add("source-code");
217        if (this._baseRevision)
218            this._baseRevision.requestContent(step1.bind(this));
219        else
220            this._revision.uiSourceCode.requestOriginalContent(step1.bind(this));
221
222        /**
223         * @param {?string} baseContent
224         * @this {WebInspector.RevisionHistoryTreeElement}
225         */
226        function step1(baseContent)
227        {
228            this._revision.requestContent(step2.bind(this, baseContent));
229        }
230
231        /**
232         * @param {?string} baseContent
233         * @param {?string} newContent
234         * @this {WebInspector.RevisionHistoryTreeElement}
235         */
236        function step2(baseContent, newContent)
237        {
238            var baseLines = difflib.stringAsLines(baseContent);
239            var newLines = difflib.stringAsLines(newContent);
240            var sm = new difflib.SequenceMatcher(baseLines, newLines);
241            var opcodes = sm.get_opcodes();
242            var lastWasSeparator = false;
243
244            for (var idx = 0; idx < opcodes.length; idx++) {
245                var code = opcodes[idx];
246                var change = code[0];
247                var b = code[1];
248                var be = code[2];
249                var n = code[3];
250                var ne = code[4];
251                var rowCount = Math.max(be - b, ne - n);
252                var topRows = [];
253                var bottomRows = [];
254                for (var i = 0; i < rowCount; i++) {
255                    if (change === "delete" || (change === "replace" && b < be)) {
256                        var lineNumber = b++;
257                        this._createLine(lineNumber, null, baseLines[lineNumber], "removed");
258                        lastWasSeparator = false;
259                    }
260
261                    if (change === "insert" || (change === "replace" && n < ne)) {
262                        var lineNumber = n++;
263                        this._createLine(null, lineNumber, newLines[lineNumber], "added");
264                        lastWasSeparator = false;
265                    }
266
267                    if (change === "equal") {
268                        b++;
269                        n++;
270                        if (!lastWasSeparator)
271                            this._createLine(null, null, "    \u2026", "separator");
272                        lastWasSeparator = true;
273                    }
274                }
275            }
276        }
277    },
278
279    oncollapse: function()
280    {
281        this._revertElement.remove();
282    },
283
284    /**
285     * @param {?number} baseLineNumber
286     * @param {?number} newLineNumber
287     * @param {string} lineContent
288     * @param {string} changeType
289     */
290    _createLine: function(baseLineNumber, newLineNumber, lineContent, changeType)
291    {
292        var child = new TreeElement("", null, false);
293        child.selectable = false;
294        this.appendChild(child);
295        var lineElement = document.createElement("span");
296
297        function appendLineNumber(lineNumber)
298        {
299            var numberString = lineNumber !== null ? numberToStringWithSpacesPadding(lineNumber + 1, 4) : "    ";
300            var lineNumberSpan = document.createElement("span");
301            lineNumberSpan.classList.add("webkit-line-number");
302            lineNumberSpan.textContent = numberString;
303            child.listItemElement.appendChild(lineNumberSpan);
304        }
305
306        appendLineNumber(baseLineNumber);
307        appendLineNumber(newLineNumber);
308
309        var contentSpan = document.createElement("span");
310        contentSpan.textContent = lineContent;
311        child.listItemElement.appendChild(contentSpan);
312        child.listItemElement.classList.add("revision-history-line");
313        child.listItemElement.classList.add("revision-history-line-" + changeType);
314    },
315
316    allowRevert: function()
317    {
318        this._revertElement.classList.remove("hidden");
319    },
320
321    __proto__: TreeElement.prototype
322}
323