mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-05 19:41:12 +08:00
Since we record changed line numbers from formatted sources, we should always compare formatted current line number against the changed line numbers. This CL also retrieves the formatted mapping from the diff call site and stores it for said comparison. Bug: chromium:1268754 Change-Id: I9f0bdd7f081b17d15a1f80ce788de54defb6717b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3466037 Reviewed-by: Mathias Bynens <mathias@chromium.org> Commit-Queue: Changhao Han <changhaohan@chromium.org>
326 lines
12 KiB
TypeScript
326 lines
12 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 Diff from '../../third_party/diff/diff.js';
|
|
import * as FormatterModule from '../formatter/formatter.js';
|
|
import * as Persistence from '../persistence/persistence.js';
|
|
import * as Workspace from '../workspace/workspace.js';
|
|
|
|
interface DiffRequestOptions {
|
|
shouldFormatDiff: boolean;
|
|
}
|
|
|
|
interface DiffResponse {
|
|
diff: Diff.Diff.DiffArray;
|
|
formattedCurrentMapping?: FormatterModule.ScriptFormatter.FormatterSourceMapping;
|
|
}
|
|
|
|
export class WorkspaceDiffImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
private readonly uiSourceCodeDiffs: WeakMap<Workspace.UISourceCode.UISourceCode, UISourceCodeDiff>;
|
|
private readonly loadingUISourceCodes:
|
|
Map<Workspace.UISourceCode.UISourceCode, Promise<[string | null, string|null]>>;
|
|
private readonly modifiedUISourceCodesInternal: Set<Workspace.UISourceCode.UISourceCode>;
|
|
|
|
constructor(workspace: Workspace.Workspace.WorkspaceImpl) {
|
|
super();
|
|
this.uiSourceCodeDiffs = new WeakMap();
|
|
|
|
this.loadingUISourceCodes = new Map();
|
|
|
|
this.modifiedUISourceCodesInternal = new Set();
|
|
workspace.addEventListener(Workspace.Workspace.Events.WorkingCopyChanged, this.uiSourceCodeChanged, this);
|
|
workspace.addEventListener(Workspace.Workspace.Events.WorkingCopyCommitted, this.uiSourceCodeChanged, this);
|
|
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this.uiSourceCodeAdded, this);
|
|
workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, this.uiSourceCodeRemoved, this);
|
|
workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, this.projectRemoved, this);
|
|
workspace.uiSourceCodes().forEach(this.updateModifiedState.bind(this));
|
|
}
|
|
|
|
requestDiff(uiSourceCode: Workspace.UISourceCode.UISourceCode, diffRequestOptions: DiffRequestOptions):
|
|
Promise<DiffResponse|null> {
|
|
return this.uiSourceCodeDiff(uiSourceCode).requestDiff(diffRequestOptions);
|
|
}
|
|
|
|
subscribeToDiffChange(uiSourceCode: Workspace.UISourceCode.UISourceCode, callback: () => void, thisObj?: Object):
|
|
void {
|
|
this.uiSourceCodeDiff(uiSourceCode).addEventListener(UISourceCodeDiffEvents.DiffChanged, callback, thisObj);
|
|
}
|
|
|
|
unsubscribeFromDiffChange(uiSourceCode: Workspace.UISourceCode.UISourceCode, callback: () => void, thisObj?: Object):
|
|
void {
|
|
this.uiSourceCodeDiff(uiSourceCode).removeEventListener(UISourceCodeDiffEvents.DiffChanged, callback, thisObj);
|
|
}
|
|
|
|
modifiedUISourceCodes(): Workspace.UISourceCode.UISourceCode[] {
|
|
return Array.from(this.modifiedUISourceCodesInternal);
|
|
}
|
|
|
|
isUISourceCodeModified(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
|
|
return this.modifiedUISourceCodesInternal.has(uiSourceCode) || this.loadingUISourceCodes.has(uiSourceCode);
|
|
}
|
|
|
|
private uiSourceCodeDiff(uiSourceCode: Workspace.UISourceCode.UISourceCode): UISourceCodeDiff {
|
|
let diff = this.uiSourceCodeDiffs.get(uiSourceCode);
|
|
if (!diff) {
|
|
diff = new UISourceCodeDiff(uiSourceCode);
|
|
this.uiSourceCodeDiffs.set(uiSourceCode, diff);
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
private uiSourceCodeChanged(
|
|
event: Common.EventTarget.EventTargetEvent<{uiSourceCode: Workspace.UISourceCode.UISourceCode}>): void {
|
|
const uiSourceCode = event.data.uiSourceCode;
|
|
void this.updateModifiedState(uiSourceCode);
|
|
}
|
|
|
|
private uiSourceCodeAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
|
|
const uiSourceCode = event.data;
|
|
void this.updateModifiedState(uiSourceCode);
|
|
}
|
|
|
|
private uiSourceCodeRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
|
|
const uiSourceCode = event.data;
|
|
this.removeUISourceCode(uiSourceCode);
|
|
}
|
|
|
|
private projectRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.Project>): void {
|
|
const project = event.data;
|
|
for (const uiSourceCode of project.uiSourceCodes()) {
|
|
this.removeUISourceCode(uiSourceCode);
|
|
}
|
|
}
|
|
|
|
private removeUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
|
this.loadingUISourceCodes.delete(uiSourceCode);
|
|
const uiSourceCodeDiff = this.uiSourceCodeDiffs.get(uiSourceCode);
|
|
if (uiSourceCodeDiff) {
|
|
uiSourceCodeDiff.dispose = true;
|
|
}
|
|
this.markAsUnmodified(uiSourceCode);
|
|
}
|
|
|
|
private markAsUnmodified(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
|
this.uiSourceCodeProcessedForTest();
|
|
if (this.modifiedUISourceCodesInternal.delete(uiSourceCode)) {
|
|
this.dispatchEventToListeners(Events.ModifiedStatusChanged, {uiSourceCode, isModified: false});
|
|
}
|
|
}
|
|
|
|
private markAsModified(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
|
|
this.uiSourceCodeProcessedForTest();
|
|
if (this.modifiedUISourceCodesInternal.has(uiSourceCode)) {
|
|
return;
|
|
}
|
|
this.modifiedUISourceCodesInternal.add(uiSourceCode);
|
|
this.dispatchEventToListeners(Events.ModifiedStatusChanged, {uiSourceCode, isModified: true});
|
|
}
|
|
|
|
private uiSourceCodeProcessedForTest(): void {
|
|
}
|
|
|
|
private async updateModifiedState(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
|
|
this.loadingUISourceCodes.delete(uiSourceCode);
|
|
|
|
if (uiSourceCode.project().type() !== Workspace.Workspace.projectTypes.Network) {
|
|
this.markAsUnmodified(uiSourceCode);
|
|
return;
|
|
}
|
|
if (uiSourceCode.isDirty()) {
|
|
this.markAsModified(uiSourceCode);
|
|
return;
|
|
}
|
|
if (!uiSourceCode.hasCommits()) {
|
|
this.markAsUnmodified(uiSourceCode);
|
|
return;
|
|
}
|
|
|
|
const contentsPromise = Promise.all([
|
|
this.requestOriginalContentForUISourceCode(uiSourceCode),
|
|
uiSourceCode.requestContent().then(deferredContent => deferredContent.content),
|
|
]);
|
|
|
|
this.loadingUISourceCodes.set(uiSourceCode, contentsPromise);
|
|
const contents = await contentsPromise;
|
|
if (this.loadingUISourceCodes.get(uiSourceCode) !== contentsPromise) {
|
|
return;
|
|
}
|
|
this.loadingUISourceCodes.delete(uiSourceCode);
|
|
|
|
if (contents[0] !== null && contents[1] !== null && contents[0] !== contents[1]) {
|
|
this.markAsModified(uiSourceCode);
|
|
} else {
|
|
this.markAsUnmodified(uiSourceCode);
|
|
}
|
|
}
|
|
|
|
requestOriginalContentForUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<string|null> {
|
|
return this.uiSourceCodeDiff(uiSourceCode).originalContent();
|
|
}
|
|
|
|
revertToOriginal(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
|
|
function callback(content: string|null): void {
|
|
if (typeof content !== 'string') {
|
|
return;
|
|
}
|
|
|
|
uiSourceCode.addRevision(content);
|
|
}
|
|
|
|
Host.userMetrics.actionTaken(Host.UserMetrics.Action.RevisionApplied);
|
|
return this.requestOriginalContentForUISourceCode(uiSourceCode).then(callback);
|
|
}
|
|
}
|
|
|
|
export const enum Events {
|
|
ModifiedStatusChanged = 'ModifiedStatusChanged',
|
|
}
|
|
|
|
export interface ModifiedStatusChangedEvent {
|
|
uiSourceCode: Workspace.UISourceCode.UISourceCode;
|
|
isModified: boolean;
|
|
}
|
|
|
|
export type EventTypes = {
|
|
[Events.ModifiedStatusChanged]: ModifiedStatusChangedEvent,
|
|
};
|
|
|
|
export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourceCodeDiffEventTypes> {
|
|
private uiSourceCode: Workspace.UISourceCode.UISourceCode;
|
|
private requestDiffPromise: Promise<DiffResponse|null>|null;
|
|
private pendingChanges: number|null;
|
|
dispose: boolean;
|
|
constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode) {
|
|
super();
|
|
this.uiSourceCode = uiSourceCode;
|
|
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.uiSourceCodeChanged, this);
|
|
uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted, this.uiSourceCodeChanged, this);
|
|
this.requestDiffPromise = null;
|
|
this.pendingChanges = null;
|
|
this.dispose = false;
|
|
}
|
|
|
|
private uiSourceCodeChanged(): void {
|
|
if (this.pendingChanges) {
|
|
clearTimeout(this.pendingChanges);
|
|
this.pendingChanges = null;
|
|
}
|
|
this.requestDiffPromise = null;
|
|
|
|
const content = this.uiSourceCode.content();
|
|
const delay = (!content || content.length < 65536) ? 0 : UpdateTimeout;
|
|
this.pendingChanges = window.setTimeout(emitDiffChanged.bind(this), delay);
|
|
|
|
function emitDiffChanged(this: UISourceCodeDiff): void {
|
|
if (this.dispose) {
|
|
return;
|
|
}
|
|
this.dispatchEventToListeners(UISourceCodeDiffEvents.DiffChanged);
|
|
this.pendingChanges = null;
|
|
}
|
|
}
|
|
|
|
requestDiff(diffRequestOptions: DiffRequestOptions): Promise<DiffResponse|null> {
|
|
if (!this.requestDiffPromise) {
|
|
this.requestDiffPromise = this.innerRequestDiff(diffRequestOptions);
|
|
}
|
|
return this.requestDiffPromise;
|
|
}
|
|
|
|
async originalContent(): Promise<string|null> {
|
|
const originalNetworkContent =
|
|
Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().originalContentForUISourceCode(
|
|
this.uiSourceCode);
|
|
if (originalNetworkContent) {
|
|
return originalNetworkContent;
|
|
}
|
|
|
|
const content = await this.uiSourceCode.project().requestFileContent(this.uiSourceCode);
|
|
return content.content || ('error' in content && content.error) || '';
|
|
}
|
|
|
|
private async innerRequestDiff({shouldFormatDiff}: DiffRequestOptions): Promise<DiffResponse|null> {
|
|
if (this.dispose) {
|
|
return null;
|
|
}
|
|
|
|
let baseline = await this.originalContent();
|
|
if (baseline === null) {
|
|
return null;
|
|
}
|
|
if (baseline.length > 1024 * 1024) {
|
|
return null;
|
|
}
|
|
// ------------ ASYNC ------------
|
|
if (this.dispose) {
|
|
return null;
|
|
}
|
|
|
|
let current = this.uiSourceCode.workingCopy();
|
|
if (!current && !this.uiSourceCode.contentLoaded()) {
|
|
current = ((await this.uiSourceCode.requestContent()).content as string);
|
|
}
|
|
|
|
if (current.length > 1024 * 1024) {
|
|
return null;
|
|
}
|
|
|
|
if (this.dispose) {
|
|
return null;
|
|
}
|
|
|
|
if (current === null || baseline === null) {
|
|
return null;
|
|
}
|
|
let formattedCurrentMapping;
|
|
if (shouldFormatDiff) {
|
|
baseline = (await FormatterModule.ScriptFormatter.format(
|
|
this.uiSourceCode.contentType(), this.uiSourceCode.mimeType(), baseline))
|
|
.formattedContent;
|
|
const formatCurrentResult = await FormatterModule.ScriptFormatter.format(
|
|
this.uiSourceCode.contentType(), this.uiSourceCode.mimeType(), current);
|
|
current = formatCurrentResult.formattedContent;
|
|
formattedCurrentMapping = formatCurrentResult.formattedMapping;
|
|
}
|
|
const reNewline = /\r\n?|\n/;
|
|
const diff = Diff.Diff.DiffWrapper.lineDiff(baseline.split(reNewline), current.split(reNewline));
|
|
return {
|
|
diff,
|
|
formattedCurrentMapping,
|
|
};
|
|
}
|
|
}
|
|
|
|
// TODO(crbug.com/1167717): Make this a const enum again
|
|
// eslint-disable-next-line rulesdir/const_enum
|
|
export enum UISourceCodeDiffEvents {
|
|
DiffChanged = 'DiffChanged',
|
|
}
|
|
|
|
export type UISourceCodeDiffEventTypes = {
|
|
[UISourceCodeDiffEvents.DiffChanged]: void,
|
|
};
|
|
|
|
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
let _instance: WorkspaceDiffImpl|null = null;
|
|
|
|
export function workspaceDiff(): WorkspaceDiffImpl {
|
|
if (!_instance) {
|
|
_instance = new WorkspaceDiffImpl(Workspace.Workspace.WorkspaceImpl.instance());
|
|
}
|
|
return _instance;
|
|
}
|
|
|
|
export class DiffUILocation {
|
|
uiSourceCode: Workspace.UISourceCode.UISourceCode;
|
|
constructor(uiSourceCode: Workspace.UISourceCode.UISourceCode) {
|
|
this.uiSourceCode = uiSourceCode;
|
|
}
|
|
}
|
|
|
|
export const UpdateTimeout = 200;
|