mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-07 04:26:32 +08:00
This CL includes the following fixes/UX improvements: - Allow user selection and clicking to source in Changes tab - Match green change indicator with multiple changed lines - Move style section bottom toolbar down to leave room for copy button These come from this doc: https://docs.google.com/document/d/1DMM-oK64W0V9pbZ0fZLZ44Lxwg3OyjZ6fh3iCXNIRDY/edit Bug: 1268754, 1297825, 1317599, 1299278 Change-Id: I04a7555feea2c900db182123d248ec3294e3991f Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3607837 Reviewed-by: Mathias Bynens <mathias@chromium.org> Reviewed-by: Johan Bay <jobay@chromium.org> Commit-Queue: Changhao Han <changhaohan@chromium.org>
286 lines
11 KiB
TypeScript
286 lines
11 KiB
TypeScript
// 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.
|
|
|
|
import * as Common from '../../core/common/common.js';
|
|
import * as Host from '../../core/host/host.js';
|
|
import * as i18n from '../../core/i18n/i18n.js';
|
|
import * as Root from '../../core/root/root.js';
|
|
import type * as Formatter from '../../models/formatter/formatter.js';
|
|
import {formatCSSChangesFromDiff} from '../../panels/utils/utils.js';
|
|
import * as Diff from '../../third_party/diff/diff.js';
|
|
import * as DiffView from '../../ui/components/diff_view/diff_view.js';
|
|
import * as UI from '../../ui/legacy/legacy.js';
|
|
|
|
import changesViewStyles from './changesView.css.js';
|
|
|
|
import type * as Workspace from '../../models/workspace/workspace.js';
|
|
import * as WorkspaceDiff from '../../models/workspace_diff/workspace_diff.js';
|
|
|
|
import {ChangesSidebar, Events} from './ChangesSidebar.js';
|
|
|
|
const UIStrings = {
|
|
/**
|
|
*@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 Screen reader/tooltip label for a button in the Changes tool that copies all changes from the currently open file.
|
|
*/
|
|
copyAllChangesFromCurrentFile: 'Copy all changes from 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 the Changes tab that indicates how many lines of code have changed in the
|
|
* selected file. An insertion refers to an added line of code. The (+) is a visual cue to indicate
|
|
* lines were added (not translatable).
|
|
*/
|
|
sInsertions: '{n, plural, =1 {# insertion (+)} other {# insertions (+)}}',
|
|
/**
|
|
* @description Text in the Changes tab that indicates how many lines of code have changed in the
|
|
* selected file. A deletion refers to a removed line of code. The (-) is a visual cue to indicate
|
|
* lines were removed (not translatable).
|
|
*/
|
|
sDeletions: '{n, plural, =1 {# deletion (-)} other {# deletions (-)}}',
|
|
/**
|
|
*@description Text for a button in the Changes tool that copies all the changes from the currently open file.
|
|
*/
|
|
copy: 'Copy',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('panels/changes/ChangesView.ts', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
function diffStats(diff: Diff.Diff.DiffArray): string {
|
|
const insertions =
|
|
diff.reduce((ins, token) => ins + (token[0] === Diff.Diff.Operation.Insert ? token[1].length : 0), 0);
|
|
const deletions =
|
|
diff.reduce((ins, token) => ins + (token[0] === Diff.Diff.Operation.Delete ? token[1].length : 0), 0);
|
|
const deletionText = i18nString(UIStrings.sDeletions, {n: deletions});
|
|
const insertionText = i18nString(UIStrings.sInsertions, {n: insertions});
|
|
return `${insertionText}, ${deletionText}`;
|
|
}
|
|
|
|
let changesViewInstance: ChangesView;
|
|
|
|
export class ChangesView extends UI.Widget.VBox {
|
|
private emptyWidget: UI.EmptyWidget.EmptyWidget;
|
|
private readonly workspaceDiff: WorkspaceDiff.WorkspaceDiff.WorkspaceDiffImpl;
|
|
readonly changesSidebar: ChangesSidebar;
|
|
private selectedUISourceCode: Workspace.UISourceCode.UISourceCode|null;
|
|
#selectedSourceCodeFormattedMapping?: Formatter.ScriptFormatter.FormatterSourceMapping;
|
|
private readonly diffContainer: HTMLElement;
|
|
private readonly toolbar: UI.Toolbar.Toolbar;
|
|
private readonly diffStats: UI.Toolbar.ToolbarText;
|
|
private readonly diffView: DiffView.DiffView.DiffView;
|
|
private readonly copyButton: UI.Toolbar.ToolbarButton;
|
|
private readonly copyButtonSeparator: UI.Toolbar.ToolbarSeparator;
|
|
|
|
private constructor() {
|
|
super(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.diffContainer = mainWidget.element.createChild('div', 'diff-container');
|
|
UI.ARIAUtils.markAsTabpanel(this.diffContainer);
|
|
this.diffContainer.addEventListener('click', event => this.click(event));
|
|
|
|
this.diffView = this.diffContainer.appendChild(new DiffView.DiffView.DiffView());
|
|
|
|
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.copyButton = new UI.Toolbar.ToolbarButton(
|
|
i18nString(UIStrings.copyAllChangesFromCurrentFile), 'largeicon-copy', UIStrings.copy);
|
|
this.copyButton.addEventListener(UI.Toolbar.ToolbarButton.Events.Click, this.copyChanges.bind(this));
|
|
this.copyButtonSeparator = new UI.Toolbar.ToolbarSeparator();
|
|
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;
|
|
}
|
|
|
|
private selectedUISourceCodeChanged(): void {
|
|
this.revealUISourceCode(this.changesSidebar.selectedUISourceCode());
|
|
if (this.selectedUISourceCode?.contentType() === Common.ResourceType.resourceTypes.Stylesheet) {
|
|
this.toolbar.appendToolbarItem(this.copyButtonSeparator);
|
|
this.toolbar.appendToolbarItem(this.copyButton);
|
|
} else {
|
|
this.toolbar.removeToolbarItem(this.copyButtonSeparator);
|
|
this.toolbar.removeToolbarItem(this.copyButton);
|
|
}
|
|
}
|
|
|
|
private revert(): void {
|
|
const uiSourceCode = this.selectedUISourceCode;
|
|
if (!uiSourceCode) {
|
|
return;
|
|
}
|
|
void this.workspaceDiff.revertToOriginal(uiSourceCode);
|
|
}
|
|
|
|
private async copyChanges(): Promise<void> {
|
|
const uiSourceCode = this.selectedUISourceCode;
|
|
if (!uiSourceCode) {
|
|
return;
|
|
}
|
|
const diffResponse = await this.workspaceDiff.requestDiff(uiSourceCode, {shouldFormatDiff: true});
|
|
// Diff array with real diff will contain at least 2 lines.
|
|
if (!diffResponse || diffResponse?.diff.length < 2) {
|
|
return;
|
|
}
|
|
const changes = await formatCSSChangesFromDiff(diffResponse.diff);
|
|
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(changes);
|
|
}
|
|
|
|
private click(event: MouseEvent): void {
|
|
if (!this.selectedUISourceCode) {
|
|
return;
|
|
}
|
|
|
|
for (const target of event.composedPath()) {
|
|
if (!(target instanceof HTMLElement)) {
|
|
continue;
|
|
}
|
|
const selection = target.ownerDocument.getSelection();
|
|
if (selection?.toString()) {
|
|
// We abort source revelation when user has text selection.
|
|
break;
|
|
}
|
|
if (target.classList.contains('diff-line-content') && target.hasAttribute('data-line-number')) {
|
|
let lineNumber = Number(target.dataset.lineNumber) - 1;
|
|
// Unfortunately, caretRangeFromPoint is broken in shadow
|
|
// roots, which makes determining the character offset more
|
|
// work than justified here.
|
|
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.PRECISE_CHANGES) &&
|
|
this.#selectedSourceCodeFormattedMapping) {
|
|
lineNumber = this.#selectedSourceCodeFormattedMapping.formattedToOriginal(lineNumber, 0)[0];
|
|
}
|
|
void Common.Revealer.reveal(this.selectedUISourceCode.uiLocation(lineNumber, 0), false);
|
|
event.consume(true);
|
|
break;
|
|
} else if (target.classList.contains('diff-listing')) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private 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;
|
|
void this.refreshDiff();
|
|
}
|
|
|
|
wasShown(): void {
|
|
void this.refreshDiff();
|
|
this.registerCSSFiles([changesViewStyles]);
|
|
}
|
|
|
|
private async refreshDiff(): Promise<void> {
|
|
if (!this.isShowing()) {
|
|
return;
|
|
}
|
|
|
|
if (!this.selectedUISourceCode) {
|
|
this.renderDiffRows();
|
|
return;
|
|
}
|
|
const uiSourceCode = this.selectedUISourceCode;
|
|
if (!uiSourceCode.contentType().isTextType()) {
|
|
this.hideDiff(i18nString(UIStrings.binaryData));
|
|
return;
|
|
}
|
|
const diffResponse = await this.workspaceDiff.requestDiff(
|
|
uiSourceCode,
|
|
{shouldFormatDiff: Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.PRECISE_CHANGES)});
|
|
if (this.selectedUISourceCode !== uiSourceCode) {
|
|
return;
|
|
}
|
|
this.#selectedSourceCodeFormattedMapping = diffResponse?.formattedCurrentMapping;
|
|
this.renderDiffRows(diffResponse?.diff);
|
|
}
|
|
|
|
private hideDiff(message: string): void {
|
|
this.diffStats.setText('');
|
|
this.toolbar.setEnabled(false);
|
|
this.diffContainer.style.display = 'none';
|
|
this.emptyWidget.text = message;
|
|
this.emptyWidget.showWidget();
|
|
}
|
|
|
|
private renderDiffRows(diff?: Diff.Diff.DiffArray): void {
|
|
if (!diff || (diff.length === 1 && diff[0][0] === Diff.Diff.Operation.Equal)) {
|
|
this.hideDiff(i18nString(UIStrings.noChanges));
|
|
} else {
|
|
this.diffStats.setText(diffStats(diff));
|
|
this.toolbar.setEnabled(true);
|
|
this.emptyWidget.hideWidget();
|
|
const mimeType = (this.selectedUISourceCode as Workspace.UISourceCode.UISourceCode).mimeType();
|
|
this.diffContainer.style.display = 'block';
|
|
this.diffView.data = {diff, mimeType};
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|