Chromium Code Reviews| Index: third_party/WebKit/Source/devtools/front_end/changes/ChangesView.js |
| diff --git a/third_party/WebKit/Source/devtools/front_end/changes/ChangesView.js b/third_party/WebKit/Source/devtools/front_end/changes/ChangesView.js |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f6834e8c58ffe135e073fadf00bbc3342d9c5657 |
| --- /dev/null |
| +++ b/third_party/WebKit/Source/devtools/front_end/changes/ChangesView.js |
| @@ -0,0 +1,460 @@ |
| +// Copyright 2017 The Chromium Authors. All rights reserved. |
| +// Use of this source code is governed by a BSD-style license that can be |
| +// found in the LICENSE file. |
| + |
| +/** |
| + * @unrestricted |
|
lushnikov
2017/02/15 02:29:54
why?
einbinder
2017/03/14 01:19:35
Gone.
|
| + * @implements {UI.ListDelegate} |
|
lushnikov
2017/02/15 02:29:54
template pls
einbinder
2017/03/14 01:19:36
Gone.
|
| + */ |
| +Changes.ChangesView = class extends UI.VBox { |
| + constructor() { |
| + super(); |
|
lushnikov
2017/02/15 02:29:54
super(true)
einbinder
2017/03/14 01:19:36
Done.
|
| + Changes.ChangesView.SharedInstance = this; |
| + this.registerRequiredCSS('changes/changesView.css'); |
| + var splitWidget = new UI.SplitWidget(true, false); |
| + var mainWidget = new UI.Widget(); |
| + splitWidget.setMainWidget(mainWidget); |
| + |
| + /** @type {?Workspace.UISourceCode} */ |
| + this._uiSourceCode = null; |
| + |
| + /** @type {!Array<!Changes.ChangesView.Row>} */ |
| + this._rows = []; |
| + |
| + this._maxLineDigits = 1; |
| + |
| + |
|
lushnikov
2017/02/15 02:29:54
two lines
einbinder
2017/03/14 01:19:36
Done.
|
| + var code = mainWidget.element.createChild('div', 'code'); |
| + this._editor = new TextEditor.CodeMirrorTextEditor( |
| + {lineNumbers: true, lineWrapping: false, mimeType: 'devtools-diff', maxHighlightLength: Infinity}); |
| + this._editor.setReadOnly(true); |
| + this._editor.show(code); |
| + this._editor.setLineNumberFormatter(() => ''); |
| + |
| + this._toolbar = new UI.Toolbar('changes-toolbar', mainWidget.element); |
| + var revertButton = new UI.ToolbarButton(Common.UIString('Revert all changes'), 'largeicon-undo'); |
| + revertButton.addEventListener(UI.ToolbarButton.Events.Click, this._revert.bind(this)); |
| + this._toolbar.appendToolbarItem(revertButton); |
| + this._diffStats = new UI.ToolbarText(''); |
| + this._toolbar.appendToolbarItem(this._diffStats); |
| + this._toolbar.setEnabled(false); |
| + |
| + this._navigator = new Changes.ChangesNavigator(); |
| + splitWidget.setSidebarWidget(this._navigator); |
| + splitWidget.show(this.element); |
| + |
| + Workspace.workspace.addEventListener( |
| + Workspace.Workspace.Events.WorkingCopyCommittedByUser, this._refreshUISourceCode, this); |
| + Workspace.workspace.addEventListener( |
| + Workspace.Workspace.Events.WorkingCopyChanged, this._refreshUISourceCode, this); |
| + } |
| + |
| + /** |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + */ |
| + static showChanges(uiSourceCode) { |
| + UI.viewManager.showView('changes.changes'); |
| + var changesView = |
| + /** @type {!Changes.ChangesView} */ (self.runtime.sharedInstance(Changes.ChangesView)); |
| + this._navigator.revealUISourceCode(uiSourceCode, true); |
| + changesView._revealUISourceCode(uiSourceCode); |
| + } |
| + |
| + _revert() { |
| + var uiSourceCode = this._uiSourceCode; |
| + if (!uiSourceCode) |
| + return; |
| + uiSourceCode.requestOriginalContent().then(original => uiSourceCode.addRevision(original || '')); |
| + } |
| + |
| + /** |
| + * @param {!Common.Event} event |
| + */ |
| + _refreshUISourceCode(event) { |
| + var uiSourceCode = /** @type !Workspace.UISourceCode */ (event.data.uiSourceCode); |
| + this._navigator.refreshUISourceCode(uiSourceCode); |
| + if (uiSourceCode === this._uiSourceCode) |
| + this._revealUISourceCode(uiSourceCode); |
| + } |
| + |
| + /** |
| + * @param {?Workspace.UISourceCode} uiSourceCode |
| + */ |
| + _revealUISourceCode(uiSourceCode) { |
| + var scrollTop = 0; |
| + if (this._uiSourceCode === uiSourceCode) { |
| + scrollTop = this._editor.element.scrollTop; |
| + } else { |
| + this._diffStats.setText('Loading..'); |
|
lushnikov
2017/02/15 02:29:54
UIString
einbinder
2017/03/14 01:19:36
Done.
|
| + this._editor.setText(''); |
| + } |
| + |
| + this._uiSourceCode = uiSourceCode; |
| + this._toolbar.setEnabled(!!uiSourceCode); |
| + |
| + if (!uiSourceCode) { |
| + this._rows = []; |
| + this._diffStats.setText(''); |
| + this._editor.setText(''); |
| + return; |
| + } |
| + uiSourceCode.requestOriginalContent().then(originalContent => { |
| + if (this._uiSourceCode !== uiSourceCode) |
| + return; |
| + this._renderRows(originalContent || '', uiSourceCode.workingCopy()); |
| + this._editor.element.scrollTop = scrollTop; |
| + }); |
| + } |
| + |
| + /** |
| + * @param {string} originalContent |
| + * @param {string} currentContent |
| + */ |
| + _renderRows(originalContent, currentContent) { |
| + this._rows = []; |
| + var lineSeparator = originalContent.indexOf('\r\n') >= 0 ? '\r\n' : '\n'; |
| + var originalLines = originalContent.split(lineSeparator); |
| + var currentLines = currentContent.split(lineSeparator); |
| + |
| + var insertions = 0; |
| + var deletions = 0; |
| + this._maxLineDigits = Math.ceil(Math.log10(Math.max(originalLines.length, currentLines.length))); |
| + |
| + var diff = Diff.Diff.lineDiff(originalLines, currentLines); |
|
lushnikov
2017/02/15 02:29:54
let's extract this and use the entity as a target
|
| + var currentLineNumber = 0; |
| + var baselineLineNumber = 0; |
| + var paddingLines = 3; |
| + |
| + for (var i = 0; i < diff.length; ++i) { |
| + var token = diff[i]; |
| + switch (token[0]) { |
| + case Diff.Diff.Operation.Equal: |
| + this._rows.pushAll(createEqualRows(token[1], i === 0, i === diff.length - 1)); |
| + break; |
| + case Diff.Diff.Operation.Insert: |
| + for (var line in token[1]) |
| + this._rows.push(createRow(line, 'addition')); |
| + break; |
| + case Diff.Diff.Operation.Delete: |
| + if (diff[i + 1] && diff[i + 1][0] === Diff.Diff.Operation.Insert) { |
| + this._rows.pushAll(createModifyRows(token[1].join(lineSeparator), diff[i + 1][1].join(lineSeparator))); |
| + i++; |
| + } else { |
| + for (var line in token[1]) |
| + this._rows.push(createRow(line, 'addition')); |
| + } |
| + break; |
| + } |
| + } |
| + Changes.ChangesView._currentContent = this._rows; |
| + this._editor.setText(this._rows.map(row => row.content.map(t => t.text).join('')).join(lineSeparator)); |
| + this._editor.setLineNumberFormatter(this._lineFormatter.bind(this)); |
| + |
| + this._diffStats.setText(Common.UIString( |
| + '%d insertion%s (+), %d deletion%s (-)', insertions, insertions > 1 ? 's' : '', deletions, |
| + deletions > 1 ? 's' : '')); |
| + |
| + /** |
| + * @param {!Array<string>} lines |
| + * @param {boolean} atStart |
| + * @param {boolean} atEnd |
| + * @return {!Array<!Changes.ChangesView.Row>}} |
| + */ |
| + function createEqualRows(lines, atStart, atEnd) { |
| + var equalRows = []; |
| + if (!atStart) { |
| + for (var i = 0; i < paddingLines && i < lines.length; i++) |
| + equalRows.push(createRow(lines[i], 'equal')); |
| + if (lines.length > paddingLines * 2 + 1 && !atEnd) { |
| + equalRows.push(createRow( |
| + Common.UIString('( \u2026 Skipping ') + (lines.length - paddingLines * 2) + |
| + Common.UIString(' matching lines \u2026 )'), |
| + 'spacer')); |
| + } |
| + } |
| + if (!atEnd) { |
| + var start = Math.max(lines.length - paddingLines - 1, atStart ? 0 : paddingLines); |
| + baselineLineNumber += start - Math.min(atStart ? 0 : paddingLines, lines.length); |
| + currentLineNumber += start - Math.min(atStart ? 0 : paddingLines, lines.length); |
| + for (var i = start; i < lines.length; i++) |
| + equalRows.push(createRow(lines[i], 'equal')); |
| + } |
| + return equalRows; |
| + } |
| + |
| + /** |
| + * @param {string} before |
| + * @param {string} after |
| + * @return {!Array<!Changes.ChangesView.Row>}} |
| + */ |
| + function createModifyRows(before, after) { |
| + var internalDiff = Diff.Diff.charDiff(before, after); |
| + var deletionRows = [createRow('', 'deletion')]; |
| + var insertionRows = [createRow('', 'addition')]; |
| + |
| + for (var token of internalDiff) { |
| + var text = token[1]; |
| + var type = token[0]; |
| + var className = type === Diff.Diff.Operation.Equal ? '' : 'double'; |
| + var first = true; |
| + for (var line of text.split(lineSeparator)) { |
| + if (first) { |
| + first = false; |
| + } else { |
| + if (type !== Diff.Diff.Operation.Insert) |
| + deletionRows.push(createRow('', 'deletion')); |
| + if (type !== Diff.Diff.Operation.Delete) |
| + insertionRows.push(createRow('', 'addition')); |
| + } |
| + if (line) { |
| + if (type !== Diff.Diff.Operation.Insert) |
| + deletionRows[deletionRows.length - 1].content.push({text: line, className: className}); |
| + |
| + if (type !== Diff.Diff.Operation.Delete) |
| + insertionRows[insertionRows.length - 1].content.push({text: line, className: className}); |
| + } |
| + } |
| + } |
| + return deletionRows.concat(insertionRows); |
| + } |
| + |
| + /** |
| + * @param {string} text |
| + * @param {string} className |
| + * @return {!Changes.ChangesView.Row} |
| + */ |
| + function createRow(text, className) { |
| + if (className === 'addition') { |
| + currentLineNumber++; |
| + insertions++; |
| + } |
| + if (className === 'deletion') { |
| + baselineLineNumber++; |
| + deletions++; |
| + } |
| + if (className === 'equal') { |
| + baselineLineNumber++; |
| + currentLineNumber++; |
| + } |
| + return { |
| + base: baselineLineNumber, |
| + current: currentLineNumber, |
| + content: text ? [{text: text, className: 'double'}] : [], |
| + className: className, |
| + loc: currentLineNumber |
| + }; |
| + } |
| + } |
| + |
| + /** |
| + * @param {number} lineNumber |
| + * @return {string} |
| + */ |
| + _lineFormatter(lineNumber) { |
| + var row = this._rows[lineNumber - 1]; |
| + if (!row) |
| + return spacesPadding(this._maxLineDigits * 2 + 1); |
| + var showBaseNumber = row.className === 'deletion'; |
| + var showCurrentNumber = row.className === 'addition'; |
| + if (row.className === 'equal') { |
| + showBaseNumber = true; |
| + showCurrentNumber = true; |
| + } |
| + var base = showBaseNumber ? numberToStringWithSpacesPadding(row.base, this._maxLineDigits) : |
| + spacesPadding(this._maxLineDigits); |
| + var current = showCurrentNumber ? numberToStringWithSpacesPadding(row.current, this._maxLineDigits) : |
| + spacesPadding(this._maxLineDigits); |
| + return base + spacesPadding(1) + current; |
| + } |
| + |
| + /** |
| + * @param {number} baselineLineNumber |
| + * @param {number} currentLineNumber |
| + * @param {string} text |
| + * @param {string=} className |
| + * @return {!Element} |
| + */ |
| + _createRow(baselineLineNumber, currentLineNumber, text, className) { |
| + var element = createElementWithClass('div', className); |
| + element.createChild('span', 'number').textContent = baselineLineNumber ? |
| + numberToStringWithSpacesPadding(baselineLineNumber, this._maxLineDigits) : |
| + spacesPadding(this._maxLineDigits); |
| + element.createChild('span', 'number').textContent = currentLineNumber ? |
| + numberToStringWithSpacesPadding(currentLineNumber, this._maxLineDigits) : |
| + spacesPadding(this._maxLineDigits); |
| + element.createChild('span', 'double').textContent = text; |
| + return element; |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!Element} item |
| + * @return {!Element} |
| + */ |
| + createElementForItem(item) { |
| + return item; |
| + } |
| + |
| + /** |
| + * @override |
| + * @return {number} |
| + */ |
| + heightForItem() { |
| + return 0; |
| + } |
| + |
| + /** |
| + * @override |
| + * @return {boolean} |
| + */ |
| + isItemSelectable(item) { |
| + return false; |
| + } |
| + |
| + /** |
| + * @override |
| + */ |
| + selectedItemChanged() { |
| + } |
| +}; |
| + |
| +/** @typedef {!{base: number, current: number, content: !Array<!{text: string, className: string}>, className: string, loc: number}} */ |
| +Changes.ChangesView.Row; |
| + |
| +Changes.ChangesNavigator = class extends Sources.NavigatorView { |
| + constructor() { |
| + super(); |
| + this.dontGroup(); |
| + this.element.classList.add('changes-navigator'); |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + * @return {boolean} |
| + */ |
| + accept(uiSourceCode) { |
| + if (uiSourceCode.project().type() !== Workspace.projectTypes.Network) |
| + return false; |
| + |
| + if (!uiSourceCode.mightHaveChanges) |
| + return false; |
| + uiSourceCode.hasChanges().then(hasChanges => { |
| + if (!hasChanges) |
| + this.refreshUISourceCode(uiSourceCode); |
| + }); |
| + return true; |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + * @param {boolean} focusSource |
| + */ |
| + revealSource(uiSourceCode, focusSource) { |
| + Changes.ChangesView.SharedInstance._revealUISourceCode(uiSourceCode); |
| + if (focusSource) |
| + Changes.ChangesView.SharedInstance._editor.focus(); |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + */ |
| + uiSourceCodeAdded(uiSourceCode) { |
| + if (!Changes.ChangesView.SharedInstance._uiSourceCode) |
| + this.revealSource(uiSourceCode, false); |
| + } |
| + |
| + /** |
| + * @override |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + */ |
| + uiSourceCodeRemoved(uiSourceCode) { |
| + if (Changes.ChangesView.SharedInstance._uiSourceCode !== uiSourceCode) |
| + return; |
| + var anyUISourceCode = this.anyUISourceCode(); |
| + if (anyUISourceCode) |
| + this.revealSource(anyUISourceCode, false); |
| + } |
| +}; |
| + |
| +Changes.UISourceCodeChange = class { |
| + /** |
| + * @param {!Workspace.UISourceCode} uiSourceCode |
| + */ |
| + constructor(uiSourceCode) { |
| + this.uiSourceCode = uiSourceCode; |
| + } |
| +}; |
| + |
| +/** |
| + * @implements {Common.Revealer} |
| + */ |
| +Changes.UISourceCodeChangeRevealer = class { |
| + /** |
| + * @override |
| + * @param {!Object} uiSourceCodeChange |
| + * @param {boolean=} omitFocus |
| + * @return {!Promise} |
| + */ |
| + reveal(uiSourceCodeChange, omitFocus) { |
| + if (!(uiSourceCodeChange instanceof Changes.UISourceCodeChange)) |
| + return Promise.reject(new Error('Changes.UISourceCodeChange')); |
| + Changes.ChangesView.showChanges(uiSourceCodeChange.uiSourceCode); |
| + return Promise.resolve(); |
| + } |
| +}; |
| + |
| +Changes.ChangesView._currentContent = []; |
| +/** @typedef {!{lineNumber: number, index: number}} */ |
| +Changes.ChangesView.DiffState; |
| + |
| +CodeMirror.defineMode('devtools-diff', function() { |
|
lushnikov
2017/02/15 02:39:19
this potentially will cache the outdated diff. We
einbinder
2017/03/14 01:19:36
Done.
|
| + return { |
| + /** |
| + * @return {!Changes.ChangesView.DiffState} |
| + */ |
| + startState: function() { |
| + return {lineNumber: 0, index: 0}; |
| + }, |
| + |
| + /** |
| + * @param {!{next: function()}} stream |
| + * @param {!Changes.ChangesView.DiffState} state |
| + * @return {string} |
| + */ |
| + token: function(stream, state) { |
| + var row = Changes.ChangesView._currentContent[state.lineNumber]; |
|
lushnikov
2017/02/15 02:39:19
you probably can pass this through options of the
einbinder
2017/03/14 01:19:36
Done.
|
| + if (!row) { |
| + stream.next(); |
| + return ''; |
| + } |
| + var classes = ''; |
| + if (state.index === 0) |
| + classes += ' line-background-' + row.className + ' line-' + row.className; |
| + var chars = row.content[state.index].text.length; |
| + for (var i = 0; i < chars; i++) |
| + stream.next(); |
| + classes += ' ' + row.content[state.index].className; |
| + state.index++; |
| + if (state.index >= row.content.length) { |
| + state.lineNumber++; |
| + state.index = 0; |
| + } |
| + return classes; |
| + }, |
| + |
| + /** |
| + * @param {!Changes.ChangesView.DiffState} state |
| + * @return {string} |
| + */ |
| + blankLine: function(state) { |
| + var row = Changes.ChangesView._currentContent[state.lineNumber]; |
| + state.lineNumber++; |
| + return row ? 'line-background-' + row.className + ' line-' + row.className : ''; |
| + }, |
| + }; |
| +}); |
| + |
| +CodeMirror.defineMIME('devtools-diff', 'devtools-diff'); |