chrome-devtools-frontend
Version:
Chrome DevTools UI
359 lines (313 loc) • 13.4 kB
text/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 {UISourceCode, type UISourceCodeMetadata} 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;
/**
* Returns an iterator for the currently registered {@link UISourceCode}s for this project. When
* new {@link UISourceCode}s are added while iterating, they might show up already. When removing
* {@link UISourceCode}s while iterating, these will no longer show up, and will have no effect
* on the other entries.
*
* @return an iterator for the sources provided by this project.
*/
uiSourceCodes(): Iterable<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;
readonly #uiSourceCodes: Map<Platform.DevToolsPath.UrlString, UISourceCode>;
constructor(workspace: WorkspaceImpl, id: string, type: projectTypes, displayName: string) {
this.workspaceInternal = workspace;
this.idInternal = id;
this.typeInternal = type;
this.displayNameInternal = displayName;
this.#uiSourceCodes = new Map();
}
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.#uiSourceCodes.set(url, uiSourceCode);
this.workspaceInternal.dispatchEventToListeners(Events.UISourceCodeAdded, uiSourceCode);
return true;
}
removeUISourceCode(url: Platform.DevToolsPath.UrlString): void {
const uiSourceCode = this.#uiSourceCodes.get(url);
if (uiSourceCode === undefined) {
return;
}
this.#uiSourceCodes.delete(url);
this.workspaceInternal.dispatchEventToListeners(Events.UISourceCodeRemoved, uiSourceCode);
}
removeProject(): void {
this.workspaceInternal.removeProject(this);
this.#uiSourceCodes.clear();
}
uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
return this.#uiSourceCodes.get(url) ?? null;
}
uiSourceCodes(): Iterable<UISourceCode> {
return this.#uiSourceCodes.values();
}
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;
this.#uiSourceCodes.set(newPath, uiSourceCode);
this.#uiSourceCodes.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;
}
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;
}
findCompatibleUISourceCodes(uiSourceCode: UISourceCode): UISourceCode[] {
const url = uiSourceCode.url();
const contentType = uiSourceCode.contentType();
const result: UISourceCode[] = [];
for (const project of this.projectsInternal.values()) {
const candidate = project.uiSourceCodeForURL(url);
if (candidate && candidate.url() === url && candidate.contentType() === contentType) {
result.push(candidate);
}
}
return result;
}
uiSourceCodesForProjectType(type: projectTypes): 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,
};