mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-06 12:01:40 +08:00
This fixes the character encoding of the UrlString returned by `urlsFromParentUrlAndName()`, which is called by `WorkspaceImpl`'s `renameUISourceCode()`, to match the encoding of filenames as they are received via `fileSystemFilesChangedAddedRemoved()` from the backend. Without a matching encoding, the UISourceCode will not be found when looking it up by URL, which in turn leads to the creation of an additional duplicate UISourceCode. Fixed: 1320717 Change-Id: I39a6bda366da3ee681a77f28bb1ad3bcccc86fa5 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3613869 Reviewed-by: Simon Zünd <szuend@chromium.org> Reviewed-by: Eric Leese <leese@chromium.org> Commit-Queue: Wolfgang Beyer <wolfi@chromium.org>
381 lines
14 KiB
TypeScript
381 lines
14 KiB
TypeScript
/*
|
|
* Copyright (C) 2012 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
import * as Common from '../../core/common/common.js';
|
|
import type * as Platform from '../../core/platform/platform.js';
|
|
import type * as TextUtils from '../text_utils/text_utils.js';
|
|
|
|
import type {UISourceCodeMetadata} from './UISourceCode.js';
|
|
import {UISourceCode} from './UISourceCode.js';
|
|
|
|
export interface ProjectSearchConfig {
|
|
query(): string;
|
|
ignoreCase(): boolean;
|
|
isRegex(): boolean;
|
|
queries(): string[];
|
|
filePathMatchesFileQuery(filePath: Platform.DevToolsPath.RawPathString|
|
|
Platform.DevToolsPath.EncodedPathString|Platform.DevToolsPath.UrlString): boolean;
|
|
}
|
|
|
|
export interface Project {
|
|
workspace(): WorkspaceImpl;
|
|
id(): string;
|
|
type(): projectTypes;
|
|
isServiceProject(): boolean;
|
|
displayName(): string;
|
|
requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
|
|
requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentProvider.DeferredContent>;
|
|
canSetFileContent(): boolean;
|
|
setFileContent(uiSourceCode: UISourceCode, newContent: string, isBase64: boolean): Promise<void>;
|
|
fullDisplayName(uiSourceCode: UISourceCode): string;
|
|
mimeType(uiSourceCode: UISourceCode): string;
|
|
canRename(): boolean;
|
|
rename(
|
|
uiSourceCode: UISourceCode, newName: Platform.DevToolsPath.RawPathString,
|
|
callback:
|
|
(arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString,
|
|
arg3?: Common.ResourceType.ResourceType) => void): void;
|
|
excludeFolder(path: Platform.DevToolsPath.UrlString): void;
|
|
canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean;
|
|
createFile(path: Platform.DevToolsPath.EncodedPathString, name: string|null, content: string, isBase64?: boolean):
|
|
Promise<UISourceCode|null>;
|
|
canCreateFile(): boolean;
|
|
deleteFile(uiSourceCode: UISourceCode): void;
|
|
remove(): void;
|
|
searchInFileContent(uiSourceCode: UISourceCode, query: string, caseSensitive: boolean, isRegex: boolean):
|
|
Promise<TextUtils.ContentProvider.SearchMatch[]>;
|
|
findFilesMatchingSearchRequest(
|
|
searchConfig: ProjectSearchConfig, filesMatchingFileQuery: Platform.DevToolsPath.UrlString[],
|
|
progress: Common.Progress.Progress): Promise<string[]>;
|
|
indexContent(progress: Common.Progress.Progress): void;
|
|
uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null;
|
|
uiSourceCodes(): UISourceCode[];
|
|
}
|
|
|
|
// TODO(crbug.com/1167717): Make this a const enum again
|
|
// eslint-disable-next-line rulesdir/const_enum, @typescript-eslint/naming-convention
|
|
export enum projectTypes {
|
|
Debugger = 'debugger',
|
|
Formatter = 'formatter',
|
|
Network = 'network',
|
|
FileSystem = 'filesystem',
|
|
ContentScripts = 'contentscripts',
|
|
Service = 'service',
|
|
}
|
|
|
|
export abstract class ProjectStore implements Project {
|
|
private readonly workspaceInternal: WorkspaceImpl;
|
|
private readonly idInternal: string;
|
|
private readonly typeInternal: projectTypes;
|
|
private readonly displayNameInternal: string;
|
|
private uiSourceCodesMap: Map<Platform.DevToolsPath.UrlString, {
|
|
uiSourceCode: UISourceCode,
|
|
index: number,
|
|
}>;
|
|
private uiSourceCodesList: UISourceCode[];
|
|
|
|
constructor(workspace: WorkspaceImpl, id: string, type: projectTypes, displayName: string) {
|
|
this.workspaceInternal = workspace;
|
|
this.idInternal = id;
|
|
this.typeInternal = type;
|
|
this.displayNameInternal = displayName;
|
|
|
|
this.uiSourceCodesMap = new Map();
|
|
this.uiSourceCodesList = [];
|
|
}
|
|
|
|
id(): string {
|
|
return this.idInternal;
|
|
}
|
|
|
|
type(): projectTypes {
|
|
return this.typeInternal;
|
|
}
|
|
|
|
displayName(): string {
|
|
return this.displayNameInternal;
|
|
}
|
|
|
|
workspace(): WorkspaceImpl {
|
|
return this.workspaceInternal;
|
|
}
|
|
|
|
createUISourceCode(url: Platform.DevToolsPath.UrlString, contentType: Common.ResourceType.ResourceType):
|
|
UISourceCode {
|
|
return new UISourceCode(this, url, contentType);
|
|
}
|
|
|
|
addUISourceCode(uiSourceCode: UISourceCode): boolean {
|
|
const url = uiSourceCode.url();
|
|
if (this.uiSourceCodeForURL(url)) {
|
|
return false;
|
|
}
|
|
this.uiSourceCodesMap.set(url, {uiSourceCode: uiSourceCode, index: this.uiSourceCodesList.length});
|
|
this.uiSourceCodesList.push(uiSourceCode);
|
|
this.workspaceInternal.dispatchEventToListeners(Events.UISourceCodeAdded, uiSourceCode);
|
|
return true;
|
|
}
|
|
|
|
removeUISourceCode(url: Platform.DevToolsPath.UrlString): void {
|
|
const uiSourceCode = this.uiSourceCodeForURL(url);
|
|
if (!uiSourceCode) {
|
|
return;
|
|
}
|
|
|
|
const entry = this.uiSourceCodesMap.get(url);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
const movedUISourceCode = this.uiSourceCodesList[this.uiSourceCodesList.length - 1];
|
|
this.uiSourceCodesList[entry.index] = movedUISourceCode;
|
|
const movedEntry = this.uiSourceCodesMap.get(movedUISourceCode.url());
|
|
if (movedEntry) {
|
|
movedEntry.index = entry.index;
|
|
}
|
|
this.uiSourceCodesList.splice(this.uiSourceCodesList.length - 1, 1);
|
|
this.uiSourceCodesMap.delete(url);
|
|
this.workspaceInternal.dispatchEventToListeners(Events.UISourceCodeRemoved, entry.uiSourceCode);
|
|
}
|
|
|
|
removeProject(): void {
|
|
this.workspaceInternal.removeProject(this);
|
|
this.uiSourceCodesMap = new Map();
|
|
this.uiSourceCodesList = [];
|
|
}
|
|
|
|
uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
|
|
const entry = this.uiSourceCodesMap.get(url);
|
|
return entry ? entry.uiSourceCode : null;
|
|
}
|
|
|
|
uiSourceCodes(): UISourceCode[] {
|
|
return this.uiSourceCodesList;
|
|
}
|
|
|
|
renameUISourceCode(uiSourceCode: UISourceCode, newName: string): void {
|
|
const oldPath = uiSourceCode.url();
|
|
const newPath = uiSourceCode.parentURL() ?
|
|
Common.ParsedURL.ParsedURL.urlFromParentUrlAndName(uiSourceCode.parentURL(), newName) :
|
|
Common.ParsedURL.ParsedURL.preEncodeSpecialCharactersInPath(newName) as Platform.DevToolsPath.UrlString;
|
|
const value = this.uiSourceCodesMap.get(oldPath) as {
|
|
uiSourceCode: UISourceCode,
|
|
index: number,
|
|
};
|
|
this.uiSourceCodesMap.set(newPath, value);
|
|
this.uiSourceCodesMap.delete(oldPath);
|
|
}
|
|
|
|
// No-op implementation for a handfull of interface methods.
|
|
|
|
rename(
|
|
_uiSourceCode: UISourceCode, _newName: string,
|
|
_callback:
|
|
(arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString,
|
|
arg3?: Common.ResourceType.ResourceType) => void): void {
|
|
}
|
|
excludeFolder(_path: Platform.DevToolsPath.UrlString): void {
|
|
}
|
|
deleteFile(_uiSourceCode: UISourceCode): void {
|
|
}
|
|
remove(): void {
|
|
}
|
|
indexContent(_progress: Common.Progress.Progress): void {
|
|
}
|
|
|
|
abstract isServiceProject(): boolean;
|
|
abstract requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
|
|
abstract requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentProvider.DeferredContent>;
|
|
abstract canSetFileContent(): boolean;
|
|
abstract setFileContent(uiSourceCode: UISourceCode, newContent: string, isBase64: boolean): Promise<void>;
|
|
abstract fullDisplayName(uiSourceCode: UISourceCode): string;
|
|
abstract mimeType(uiSourceCode: UISourceCode): string;
|
|
abstract canRename(): boolean;
|
|
abstract canExcludeFolder(path: Platform.DevToolsPath.EncodedPathString): boolean;
|
|
abstract createFile(
|
|
path: Platform.DevToolsPath.EncodedPathString, name: string|null, content: string,
|
|
isBase64?: boolean): Promise<UISourceCode|null>;
|
|
abstract canCreateFile(): boolean;
|
|
abstract searchInFileContent(uiSourceCode: UISourceCode, query: string, caseSensitive: boolean, isRegex: boolean):
|
|
Promise<TextUtils.ContentProvider.SearchMatch[]>;
|
|
abstract findFilesMatchingSearchRequest(
|
|
searchConfig: ProjectSearchConfig, filesMatchingFileQuery: Platform.DevToolsPath.UrlString[],
|
|
progress: Common.Progress.Progress): Promise<string[]>;
|
|
}
|
|
|
|
let workspaceInstance: WorkspaceImpl|undefined;
|
|
|
|
export class WorkspaceImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
|
private projectsInternal: Map<string, Project>;
|
|
private hasResourceContentTrackingExtensionsInternal: boolean;
|
|
|
|
private constructor() {
|
|
super();
|
|
this.projectsInternal = new Map();
|
|
this.hasResourceContentTrackingExtensionsInternal = false;
|
|
}
|
|
|
|
static instance(opts: {forceNew: boolean|null} = {forceNew: null}): WorkspaceImpl {
|
|
const {forceNew} = opts;
|
|
if (!workspaceInstance || forceNew) {
|
|
workspaceInstance = new WorkspaceImpl();
|
|
}
|
|
|
|
return workspaceInstance;
|
|
}
|
|
|
|
static removeInstance(): void {
|
|
workspaceInstance = undefined;
|
|
}
|
|
|
|
uiSourceCode(projectId: string, url: Platform.DevToolsPath.UrlString): UISourceCode|null {
|
|
const project = this.projectsInternal.get(projectId);
|
|
return project ? project.uiSourceCodeForURL(url) : null;
|
|
}
|
|
|
|
// This method explicitly awaits the UISourceCode if not yet
|
|
// available.
|
|
uiSourceCodeForURLPromise(url: Platform.DevToolsPath.UrlString): Promise<UISourceCode> {
|
|
const uiSourceCode = this.uiSourceCodeForURL(url);
|
|
if (uiSourceCode) {
|
|
return Promise.resolve(uiSourceCode);
|
|
}
|
|
return new Promise(resolve => {
|
|
const descriptor = this.addEventListener(Events.UISourceCodeAdded, event => {
|
|
const uiSourceCode = event.data;
|
|
if (uiSourceCode.url() === url) {
|
|
this.removeEventListener(Events.UISourceCodeAdded, descriptor.listener);
|
|
resolve(uiSourceCode);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
|
|
for (const project of this.projectsInternal.values()) {
|
|
const uiSourceCode = project.uiSourceCodeForURL(url);
|
|
if (uiSourceCode) {
|
|
return uiSourceCode;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
uiSourceCodesForProjectType(type: string): UISourceCode[] {
|
|
const result: UISourceCode[] = [];
|
|
for (const project of this.projectsInternal.values()) {
|
|
if (project.type() === type) {
|
|
result.push(...project.uiSourceCodes());
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
addProject(project: Project): void {
|
|
console.assert(!this.projectsInternal.has(project.id()), `A project with id ${project.id()} already exists!`);
|
|
this.projectsInternal.set(project.id(), project);
|
|
this.dispatchEventToListeners(Events.ProjectAdded, project);
|
|
}
|
|
|
|
removeProject(project: Project): void {
|
|
this.projectsInternal.delete(project.id());
|
|
this.dispatchEventToListeners(Events.ProjectRemoved, project);
|
|
}
|
|
|
|
project(projectId: string): Project|null {
|
|
return this.projectsInternal.get(projectId) || null;
|
|
}
|
|
|
|
projects(): Project[] {
|
|
return [...this.projectsInternal.values()];
|
|
}
|
|
|
|
projectsForType(type: string): Project[] {
|
|
function filterByType(project: Project): boolean {
|
|
return project.type() === type;
|
|
}
|
|
return this.projects().filter(filterByType);
|
|
}
|
|
|
|
uiSourceCodes(): UISourceCode[] {
|
|
const result: UISourceCode[] = [];
|
|
for (const project of this.projectsInternal.values()) {
|
|
result.push(...project.uiSourceCodes());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
setHasResourceContentTrackingExtensions(hasExtensions: boolean): void {
|
|
this.hasResourceContentTrackingExtensionsInternal = hasExtensions;
|
|
}
|
|
|
|
hasResourceContentTrackingExtensions(): boolean {
|
|
return this.hasResourceContentTrackingExtensionsInternal;
|
|
}
|
|
}
|
|
|
|
// TODO(crbug.com/1167717): Make this a const enum again
|
|
// eslint-disable-next-line rulesdir/const_enum
|
|
export enum Events {
|
|
UISourceCodeAdded = 'UISourceCodeAdded',
|
|
UISourceCodeRemoved = 'UISourceCodeRemoved',
|
|
UISourceCodeRenamed = 'UISourceCodeRenamed',
|
|
WorkingCopyChanged = 'WorkingCopyChanged',
|
|
WorkingCopyCommitted = 'WorkingCopyCommitted',
|
|
WorkingCopyCommittedByUser = 'WorkingCopyCommittedByUser',
|
|
ProjectAdded = 'ProjectAdded',
|
|
ProjectRemoved = 'ProjectRemoved',
|
|
}
|
|
|
|
export interface UISourceCodeRenamedEvent {
|
|
oldURL: Platform.DevToolsPath.UrlString;
|
|
uiSourceCode: UISourceCode;
|
|
}
|
|
|
|
export interface WorkingCopyChangedEvent {
|
|
uiSourceCode: UISourceCode;
|
|
}
|
|
|
|
export interface WorkingCopyCommitedEvent {
|
|
uiSourceCode: UISourceCode;
|
|
content: string;
|
|
encoded?: boolean;
|
|
}
|
|
|
|
export type EventTypes = {
|
|
[Events.UISourceCodeAdded]: UISourceCode,
|
|
[Events.UISourceCodeRemoved]: UISourceCode,
|
|
[Events.UISourceCodeRenamed]: UISourceCodeRenamedEvent,
|
|
[Events.WorkingCopyChanged]: WorkingCopyChangedEvent,
|
|
[Events.WorkingCopyCommitted]: WorkingCopyCommitedEvent,
|
|
[Events.WorkingCopyCommittedByUser]: WorkingCopyCommitedEvent,
|
|
[Events.ProjectAdded]: Project,
|
|
[Events.ProjectRemoved]: Project,
|
|
};
|