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