UNPKG

chrome-devtools-frontend

Version:
431 lines (381 loc) 15.1 kB
// 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. /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../common/common.js'; import * as Diff from '../diff/diff.js'; import * as i18n from '../i18n/i18n.js'; import * as UI from '../ui/ui.js'; import * as Workspace from '../workspace/workspace.js'; // eslint-disable-line no-unused-vars import * as WorkspaceDiff from '../workspace_diff/workspace_diff.js'; import {ChangesSidebar, Events} from './ChangesSidebar.js'; import {ChangesTextEditor} from './ChangesTextEditor.js'; export const UIStrings = { /** *@description Screen-reader accessible name for the code editor in the Changes tool showing the user's changes. */ changesDiffViewer: 'Changes diff viewer', /** *@description Screen reader/tooltip label for a button in the Changes tool that reverts all changes to the currently open file. */ revertAllChangesToCurrentFile: 'Revert all changes to current file', /** *@description Text in Changes View of the Changes tab */ noChanges: 'No changes', /** *@description Text in Changes View of the Changes tab */ binaryData: 'Binary data', /** *@description Text in Changes View of the Changes tab when one code insertion has occurred. */ sInsertion: '1 insertion `(+)`,', /** * @description Text in Changes View of the Changes tab when multiple code insertions have * occurred. * @example {2} PH1 */ sInsertions: '{PH1} insertions `(+)`,', /** *@description Text in Changes View of the Changes tab when one code deletion has occurred. */ sDeletion: '1 deletion `(-)`', /** * @description Text in Changes View of the Changes tab when multiple code deletions have occurred. * @example {2} PH1 */ sDeletions: '{PH1} deletions `(-)`', /** *@description Text in Changes View of the Changes tab *@example {2} PH1 */ SkippingDMatchingLines: '( … Skipping {PH1} matching lines … )', }; const str_ = i18n.i18n.registerUIStrings('changes/ChangesView.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); let changesViewInstance: ChangesView; export class ChangesView extends UI.Widget.VBox { _emptyWidget: UI.EmptyWidget.EmptyWidget; _workspaceDiff: WorkspaceDiff.WorkspaceDiff.WorkspaceDiffImpl; _changesSidebar: ChangesSidebar; _selectedUISourceCode: Workspace.UISourceCode.UISourceCode|null; _diffRows: Row[]; _maxLineDigits: number; _editor: ChangesTextEditor; _toolbar: UI.Toolbar.Toolbar; _diffStats: UI.Toolbar.ToolbarText; private constructor() { super(true); this.registerRequiredCSS('changes/changesView.css', {enableLegacyPatching: true}); const splitWidget = new UI.SplitWidget.SplitWidget(true /* vertical */, false /* sidebar on left */); const mainWidget = new UI.Widget.Widget(); splitWidget.setMainWidget(mainWidget); splitWidget.show(this.contentElement); this._emptyWidget = new UI.EmptyWidget.EmptyWidget(''); this._emptyWidget.show(mainWidget.element); this._workspaceDiff = WorkspaceDiff.WorkspaceDiff.workspaceDiff(); this._changesSidebar = new ChangesSidebar(this._workspaceDiff); this._changesSidebar.addEventListener(Events.SelectedUISourceCodeChanged, this._selectedUISourceCodeChanged, this); splitWidget.setSidebarWidget(this._changesSidebar); this._selectedUISourceCode = null; this._diffRows = []; this._maxLineDigits = 1; this._editor = new ChangesTextEditor({ bracketMatchingSetting: undefined, devtoolsAccessibleName: i18nString(UIStrings.changesDiffViewer), lineNumbers: true, lineWrapping: false, mimeType: undefined, autoHeight: undefined, padBottom: undefined, maxHighlightLength: Infinity, // Avoid CodeMirror bailing out of highlighting big diffs. placeholder: undefined, lineWiseCopyCut: undefined, inputStyle: undefined, }); this._editor.setReadOnly(true); const editorContainer = mainWidget.element.createChild('div', 'editor-container'); UI.ARIAUtils.markAsTabpanel(editorContainer); this._editor.show(editorContainer); this._editor.hideWidget(); self.onInvokeElement(this._editor.element, this._click.bind(this)); this._toolbar = new UI.Toolbar.Toolbar('changes-toolbar', mainWidget.element); const revertButton = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.revertAllChangesToCurrentFile), 'largeicon-undo'); revertButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this._revert.bind(this)); this._toolbar.appendToolbarItem(revertButton); this._diffStats = new UI.Toolbar.ToolbarText(''); this._toolbar.appendToolbarItem(this._diffStats); this._toolbar.setEnabled(false); this._hideDiff(i18nString(UIStrings.noChanges)); this._selectedUISourceCodeChanged(); } static instance(opts: {forceNew: boolean|null} = {forceNew: null}): ChangesView { const {forceNew} = opts; if (!changesViewInstance || forceNew) { changesViewInstance = new ChangesView(); } return changesViewInstance; } _selectedUISourceCodeChanged(): void { this._revealUISourceCode(this._changesSidebar.selectedUISourceCode()); } _revert(): void { const uiSourceCode = this._selectedUISourceCode; if (!uiSourceCode) { return; } this._workspaceDiff.revertToOriginal(uiSourceCode); } _click(event: Event): void { const selection = this._editor.selection(); if (!selection.isEmpty() || !this._selectedUISourceCode) { return; } const row = this._diffRows[selection.startLine]; Common.Revealer.reveal( this._selectedUISourceCode.uiLocation(row.currentLineNumber - 1, selection.startColumn), false); event.consume(true); } _revealUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode|null): void { if (this._selectedUISourceCode === uiSourceCode) { return; } if (this._selectedUISourceCode) { this._workspaceDiff.unsubscribeFromDiffChange(this._selectedUISourceCode, this._refreshDiff, this); } if (uiSourceCode && this.isShowing()) { this._workspaceDiff.subscribeToDiffChange(uiSourceCode, this._refreshDiff, this); } this._selectedUISourceCode = uiSourceCode; this._refreshDiff(); } wasShown(): void { this._refreshDiff(); } _refreshDiff(): void { if (!this.isShowing()) { return; } if (!this._selectedUISourceCode) { this._renderDiffRows(null); return; } const uiSourceCode = this._selectedUISourceCode; if (!uiSourceCode.contentType().isTextType()) { this._hideDiff(i18nString(UIStrings.binaryData)); return; } this._workspaceDiff.requestDiff(uiSourceCode).then((diff: Diff.Diff.DiffArray|null): void => { if (this._selectedUISourceCode !== uiSourceCode) { return; } this._renderDiffRows(diff); }); } _hideDiff(message: string): void { this._diffStats.setText(''); this._toolbar.setEnabled(false); this._editor.hideWidget(); this._emptyWidget.text = message; this._emptyWidget.showWidget(); } _renderDiffRows(diff: Diff.Diff.DiffArray|null): void { this._diffRows = []; if (!diff || (diff.length === 1 && diff[0][0] === Diff.Diff.Operation.Equal)) { this._hideDiff(i18nString(UIStrings.noChanges)); return; } let insertions = 0; let deletions = 0; let currentLineNumber = 0; let baselineLineNumber = 0; const paddingLines = 3; const originalLines: string[] = []; const currentLines: string[] = []; for (let i = 0; i < diff.length; ++i) { const token = diff[i]; switch (token[0]) { case Diff.Diff.Operation.Equal: this._diffRows.push(...createEqualRows(token[1], i === 0, i === diff.length - 1)); originalLines.push(...token[1]); currentLines.push(...token[1]); break; case Diff.Diff.Operation.Insert: for (const line of token[1]) { this._diffRows.push(createRow(line, RowType.Addition)); } insertions += token[1].length; currentLines.push(...token[1]); break; case Diff.Diff.Operation.Delete: deletions += token[1].length; originalLines.push(...token[1]); if (diff[i + 1] && diff[i + 1][0] === Diff.Diff.Operation.Insert) { i++; this._diffRows.push(...createModifyRows(token[1].join('\n'), diff[i][1].join('\n'))); insertions += diff[i][1].length; currentLines.push(...diff[i][1]); } else { for (const line of token[1]) { this._diffRows.push(createRow(line, RowType.Deletion)); } } break; } } this._maxLineDigits = Math.ceil(Math.log10(Math.max(currentLineNumber, baselineLineNumber))); let insertionText: Common.UIString.LocalizedString|'' = ''; if (insertions === 1) { insertionText = i18nString(UIStrings.sInsertion); } else { insertionText = i18nString(UIStrings.sInsertions, {PH1: insertions}); } let deletionText: Common.UIString.LocalizedString|'' = ''; if (deletions === 1) { deletionText = i18nString(UIStrings.sDeletion); } else { deletionText = i18nString(UIStrings.sDeletions, {PH1: deletions}); } this._diffStats.setText(`${insertionText} ${deletionText}`); this._toolbar.setEnabled(true); this._emptyWidget.hideWidget(); this._editor.operation((): void => { this._editor.showWidget(); this._editor.setHighlightMode({ name: 'devtools-diff', diffRows: this._diffRows, mimeType: /** @type {!Workspace.UISourceCode.UISourceCode} */ ( this._selectedUISourceCode as Workspace.UISourceCode.UISourceCode) .mimeType(), baselineLines: originalLines, currentLines: currentLines, }); this._editor.setText( this._diffRows .map( (row: Row): string => row.tokens.map((t: {text: string, className: string}): string => t.text).join('')) .join('\n')); this._editor.setLineNumberFormatter(this._lineFormatter.bind(this)); this._editor.updateDiffGutter(this._diffRows); }); function createEqualRows(lines: string[], atStart: boolean, atEnd: boolean): Row[] { const equalRows = []; if (!atStart) { for (let i = 0; i < paddingLines && i < lines.length; i++) { equalRows.push(createRow(lines[i], RowType.Equal)); } if (lines.length > paddingLines * 2 + 1 && !atEnd) { equalRows.push(createRow( i18nString(UIStrings.SkippingDMatchingLines, {PH1: (lines.length - paddingLines * 2)}), RowType.Spacer)); } } if (!atEnd) { const start = Math.max(lines.length - paddingLines - 1, atStart ? 0 : paddingLines); let skip = lines.length - paddingLines - 1; if (!atStart) { skip -= paddingLines; } if (skip > 0) { baselineLineNumber += skip; currentLineNumber += skip; } for (let i = start; i < lines.length; i++) { equalRows.push(createRow(lines[i], RowType.Equal)); } } return equalRows; } function createModifyRows(before: string, after: string): Row[] { const internalDiff = Diff.Diff.DiffWrapper.charDiff(before, after, true /* cleanup diff */); const deletionRows = [createRow('', RowType.Deletion)]; const insertionRows = [createRow('', RowType.Addition)]; for (const token of internalDiff) { const text = token[1]; const type = token[0]; const className = type === Diff.Diff.Operation.Equal ? '' : 'inner-diff'; const lines = text.split('\n'); for (let i = 0; i < lines.length; i++) { if (i > 0 && type !== Diff.Diff.Operation.Insert) { deletionRows.push(createRow('', RowType.Deletion)); } if (i > 0 && type !== Diff.Diff.Operation.Delete) { insertionRows.push(createRow('', RowType.Addition)); } if (!lines[i]) { continue; } if (type !== Diff.Diff.Operation.Insert) { deletionRows[deletionRows.length - 1].tokens.push({text: lines[i], className}); } if (type !== Diff.Diff.Operation.Delete) { insertionRows[insertionRows.length - 1].tokens.push({text: lines[i], className}); } } } return deletionRows.concat(insertionRows); } function createRow(text: string, type: RowType): Row { if (type === RowType.Addition) { currentLineNumber++; } if (type === RowType.Deletion) { baselineLineNumber++; } if (type === RowType.Equal) { baselineLineNumber++; currentLineNumber++; } return {baselineLineNumber, currentLineNumber, tokens: text ? [{text, className: 'inner-diff'}] : [], type}; } } _lineFormatter(lineNumber: number): string { const row = this._diffRows[lineNumber - 1]; let showBaseNumber = row.type === RowType.Deletion; let showCurrentNumber = row.type === RowType.Addition; if (row.type === RowType.Equal) { showBaseNumber = true; showCurrentNumber = true; } const baseText = showBaseNumber ? String(row.baselineLineNumber) : ''; const base = baseText.padStart(this._maxLineDigits, '\xA0'); const currentText = showCurrentNumber ? String(row.currentLineNumber) : ''; const current = currentText.padStart(this._maxLineDigits, '\xA0'); return base + '\xA0' + current; } } export const enum RowType { Deletion = 'deletion', Addition = 'addition', Equal = 'equal', Spacer = 'spacer', } let diffUILocationRevealerInstance: DiffUILocationRevealer; export class DiffUILocationRevealer implements Common.Revealer.Revealer { static instance(opts: {forceNew: boolean} = {forceNew: false}): DiffUILocationRevealer { const {forceNew} = opts; if (!diffUILocationRevealerInstance || forceNew) { diffUILocationRevealerInstance = new DiffUILocationRevealer(); } return diffUILocationRevealerInstance; } async reveal(diffUILocation: Object, omitFocus?: boolean|undefined): Promise<void> { if (!(diffUILocation instanceof WorkspaceDiff.WorkspaceDiff.DiffUILocation)) { throw new Error('Internal error: not a diff ui location'); } await UI.ViewManager.ViewManager.instance().showView('changes.changes'); ChangesView.instance()._changesSidebar.selectUISourceCode(diffUILocation.uiSourceCode, omitFocus); } } export interface Token { text: string; className: string; } export interface Row { baselineLineNumber: number; currentLineNumber: number; tokens: Token[]; type: RowType; }