chrome-devtools-frontend
Version:
Chrome DevTools UI
311 lines (261 loc) • 11.2 kB
text/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 TextUtils from '../text_utils/text_utils.js';
import * as Workspace from '../workspace/workspace.js';
interface DiffResponse {
diff: Diff.Diff.DiffArray;
formattedCurrentMapping?: FormatterModule.ScriptFormatter.FormatterSourceMapping;
}
export class WorkspaceDiffImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
readonly #persistence = Persistence.Persistence.PersistenceImpl.instance();
readonly #diffs = new WeakMap<Workspace.UISourceCode.UISourceCode, UISourceCodeDiff>();
/** used in web tests */
private readonly loadingUISourceCodes =
new Map<Workspace.UISourceCode.UISourceCode, Promise<[string | null, string|null]>>();
readonly #modified = new Set<Workspace.UISourceCode.UISourceCode>();
constructor(workspace: Workspace.Workspace.WorkspaceImpl) {
super();
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): Promise<DiffResponse|null> {
return this.#uiSourceCodeDiff(uiSourceCode).requestDiff();
}
subscribeToDiffChange(uiSourceCode: Workspace.UISourceCode.UISourceCode, callback: () => void, thisObj?: Object):
void {
this.#uiSourceCodeDiff(uiSourceCode).addEventListener(UISourceCodeDiffEvents.DIFF_CHANGED, callback, thisObj);
}
unsubscribeFromDiffChange(uiSourceCode: Workspace.UISourceCode.UISourceCode, callback: () => void, thisObj?: Object):
void {
this.#uiSourceCodeDiff(uiSourceCode).removeEventListener(UISourceCodeDiffEvents.DIFF_CHANGED, callback, thisObj);
}
modifiedUISourceCodes(): Workspace.UISourceCode.UISourceCode[] {
return Array.from(this.#modified);
}
#uiSourceCodeDiff(uiSourceCode: Workspace.UISourceCode.UISourceCode): UISourceCodeDiff {
let diff = this.#diffs.get(uiSourceCode);
if (!diff) {
diff = new UISourceCodeDiff(uiSourceCode);
this.#diffs.set(uiSourceCode, diff);
}
return diff;
}
#uiSourceCodeChanged(event: Common.EventTarget.EventTargetEvent<{uiSourceCode: Workspace.UISourceCode.UISourceCode}>):
void {
const uiSourceCode = event.data.uiSourceCode;
void this.#updateModifiedState(uiSourceCode);
}
#uiSourceCodeAdded(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
void this.#updateModifiedState(uiSourceCode);
}
#uiSourceCodeRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
const uiSourceCode = event.data;
this.#removeUISourceCode(uiSourceCode);
}
#projectRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.Workspace.Project>): void {
const project = event.data;
for (const uiSourceCode of project.uiSourceCodes()) {
this.#removeUISourceCode(uiSourceCode);
}
}
#removeUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
this.loadingUISourceCodes.delete(uiSourceCode);
const uiSourceCodeDiff = this.#diffs.get(uiSourceCode);
if (uiSourceCodeDiff) {
uiSourceCodeDiff.dispose = true;
}
this.#markAsUnmodified(uiSourceCode);
}
#markAsUnmodified(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
this.uiSourceCodeProcessedForTest();
if (this.#modified.delete(uiSourceCode)) {
this.dispatchEventToListeners(Events.MODIFIED_STATUS_CHANGED, {uiSourceCode, isModified: false});
}
}
#markAsModified(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
this.uiSourceCodeProcessedForTest();
if (this.#modified.has(uiSourceCode)) {
return;
}
this.#modified.add(uiSourceCode);
this.dispatchEventToListeners(Events.MODIFIED_STATUS_CHANGED, {uiSourceCode, isModified: true});
}
private uiSourceCodeProcessedForTest(): void {
}
#shouldTrack(uiSourceCode: Workspace.UISourceCode.UISourceCode): boolean {
switch (uiSourceCode.project().type()) {
case Workspace.Workspace.projectTypes.Network:
// We track differences for all Network resources.
return this.#persistence.binding(uiSourceCode) === null;
case Workspace.Workspace.projectTypes.FileSystem:
// We track differences for FileSystem resources without bindings.
return true;
default:
return false;
}
}
async #updateModifiedState(uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<void> {
this.loadingUISourceCodes.delete(uiSourceCode);
if (!this.#shouldTrack(uiSourceCode)) {
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 {
MODIFIED_STATUS_CHANGED = 'ModifiedStatusChanged',
}
export interface ModifiedStatusChangedEvent {
uiSourceCode: Workspace.UISourceCode.UISourceCode;
isModified: boolean;
}
export interface EventTypes {
[Events.MODIFIED_STATUS_CHANGED]: ModifiedStatusChangedEvent;
}
export class UISourceCodeDiff extends Common.ObjectWrapper.ObjectWrapper<UISourceCodeDiffEventTypes> {
#uiSourceCode: Workspace.UISourceCode.UISourceCode;
#requestDiffPromise: Promise<DiffResponse|null>|null = null;
#pendingChanges: number|null = null;
dispose = false;
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);
}
#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 : 200;
this.#pendingChanges = window.setTimeout(emitDiffChanged.bind(this), delay);
function emitDiffChanged(this: UISourceCodeDiff): void {
if (this.dispose) {
return;
}
this.dispatchEventToListeners(UISourceCodeDiffEvents.DIFF_CHANGED);
this.#pendingChanges = null;
}
}
requestDiff(): Promise<DiffResponse|null> {
if (!this.#requestDiffPromise) {
this.#requestDiffPromise = this.#requestDiff();
}
return this.#requestDiffPromise;
}
async originalContent(): Promise<string|null> {
const originalNetworkContent =
Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().originalContentForUISourceCode(
this.#uiSourceCode);
if (originalNetworkContent) {
return await originalNetworkContent;
}
const content = await this.#uiSourceCode.project().requestFileContent(this.#uiSourceCode);
if (TextUtils.ContentData.ContentData.isError(content)) {
return content.error;
}
return content.asDeferedContent().content;
}
async #requestDiff(): 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;
}
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;
const formattedCurrentMapping = formatCurrentResult.formattedMapping;
const reNewline = /\r\n?|\n/;
const diff = Diff.Diff.DiffWrapper.lineDiff(baseline.split(reNewline), current.split(reNewline));
return {
diff,
formattedCurrentMapping,
};
}
}
export const enum UISourceCodeDiffEvents {
DIFF_CHANGED = 'DiffChanged',
}
export interface UISourceCodeDiffEventTypes {
[UISourceCodeDiffEvents.DIFF_CHANGED]: void;
}
let workspaceDiffImplInstance: WorkspaceDiffImpl|null = null;
export function workspaceDiff({forceNew}: {forceNew?: boolean} = {}): WorkspaceDiffImpl {
if (!workspaceDiffImplInstance || forceNew) {
workspaceDiffImplInstance = new WorkspaceDiffImpl(Workspace.Workspace.WorkspaceImpl.instance());
}
return workspaceDiffImplInstance;
}