chrome-devtools-frontend
Version:
Chrome DevTools UI
366 lines (320 loc) • 13.6 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 type {SearchConfig} from './SearchConfig.js';
import {UISourceCode, type UISourceCodeMetadata} from './UISourceCode.js';
export interface Project {
workspace(): WorkspaceImpl;
id(): string;
type(): projectTypes;
isServiceProject(): boolean;
displayName(): string;
requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentData.ContentDataOrError>;
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;
deleteDirectoryRecursively(path: Platform.DevToolsPath.EncodedPathString): Promise<boolean>;
remove(): void;
removeUISourceCode(url: Platform.DevToolsPath.UrlString): void;
searchInFileContent(uiSourceCode: UISourceCode, query: string, caseSensitive: boolean, isRegex: boolean):
Promise<TextUtils.ContentProvider.SearchMatch[]>;
findFilesMatchingSearchRequest(
searchConfig: SearchConfig, filesMatchingFileQuery: UISourceCode[],
progress: Common.Progress.Progress): Promise<Map<UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>>;
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>;
}
/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
export enum projectTypes {
Debugger = 'debugger',
Formatter = 'formatter',
Network = 'network',
FileSystem = 'filesystem',
ConnectableFileSystem = 'connectablefilesystem',
ContentScripts = 'contentscripts',
Service = 'service',
}
/* eslint-enable @typescript-eslint/naming-convention */
export abstract class ProjectStore implements Project {
readonly #workspace: WorkspaceImpl;
readonly #id: string;
readonly #type: projectTypes;
readonly #displayName: string;
readonly #uiSourceCodes = new Map<Platform.DevToolsPath.UrlString, UISourceCode>();
constructor(workspace: WorkspaceImpl, id: string, type: projectTypes, displayName: string) {
this.#workspace = workspace;
this.#id = id;
this.#type = type;
this.#displayName = displayName;
}
id(): string {
return this.#id;
}
type(): projectTypes {
return this.#type;
}
displayName(): string {
return this.#displayName;
}
workspace(): WorkspaceImpl {
return this.#workspace;
}
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.#workspace.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.#workspace.dispatchEventToListeners(Events.UISourceCodeRemoved, uiSourceCode);
}
removeProject(): void {
this.#workspace.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 handful 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 {
}
deleteDirectoryRecursively(_path: Platform.DevToolsPath.EncodedPathString): Promise<boolean> {
return Promise.resolve(false);
}
remove(): void {
}
indexContent(_progress: Common.Progress.Progress): void {
}
abstract isServiceProject(): boolean;
abstract requestMetadata(uiSourceCode: UISourceCode): Promise<UISourceCodeMetadata|null>;
abstract requestFileContent(uiSourceCode: UISourceCode): Promise<TextUtils.ContentData.ContentDataOrError>;
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: SearchConfig, filesMatchingFileQuery: UISourceCode[],
progress: Common.Progress.Progress): Promise<Map<UISourceCode, TextUtils.ContentProvider.SearchMatch[]|null>>;
}
let workspaceInstance: WorkspaceImpl|undefined;
export class WorkspaceImpl extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
#projects = new Map<string, Project>();
#hasResourceContentTrackingExtensions = false;
private constructor() {
super();
}
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.#projects.get(projectId);
return project ? project.uiSourceCodeForURL(url) : null;
}
uiSourceCodeForURL(url: Platform.DevToolsPath.UrlString): UISourceCode|null {
for (const project of this.#projects.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.#projects.values()) {
if (uiSourceCode.project().type() !== project.type()) {
continue;
}
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.#projects.values()) {
if (project.type() === type) {
for (const uiSourceCode of project.uiSourceCodes()) {
result.push(uiSourceCode);
}
}
}
return result;
}
addProject(project: Project): void {
console.assert(!this.#projects.has(project.id()), `A project with id ${project.id()} already exists!`);
this.#projects.set(project.id(), project);
this.dispatchEventToListeners(Events.ProjectAdded, project);
}
removeProject(project: Project): void {
this.#projects.delete(project.id());
this.dispatchEventToListeners(Events.ProjectRemoved, project);
}
project(projectId: string): Project|null {
return this.#projects.get(projectId) || null;
}
projectForFileSystemRoot(root: Platform.DevToolsPath.RawPathString): Project|null {
const projectId = Common.ParsedURL.ParsedURL.rawPathToUrlString(root);
return this.project(projectId);
}
projects(): Project[] {
return [...this.#projects.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.#projects.values()) {
for (const uiSourceCode of project.uiSourceCodes()) {
result.push(uiSourceCode);
}
}
return result;
}
setHasResourceContentTrackingExtensions(hasExtensions: boolean): void {
this.#hasResourceContentTrackingExtensions = hasExtensions;
}
hasResourceContentTrackingExtensions(): boolean {
return this.#hasResourceContentTrackingExtensions;
}
}
export enum Events {
/* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
UISourceCodeAdded = 'UISourceCodeAdded',
UISourceCodeRemoved = 'UISourceCodeRemoved',
UISourceCodeRenamed = 'UISourceCodeRenamed',
WorkingCopyChanged = 'WorkingCopyChanged',
WorkingCopyCommitted = 'WorkingCopyCommitted',
WorkingCopyCommittedByUser = 'WorkingCopyCommittedByUser',
ProjectAdded = 'ProjectAdded',
ProjectRemoved = 'ProjectRemoved',
/* eslint-enable @typescript-eslint/naming-convention */
}
export interface UISourceCodeRenamedEvent {
oldURL: Platform.DevToolsPath.UrlString;
uiSourceCode: UISourceCode;
}
export interface WorkingCopyChangedEvent {
uiSourceCode: UISourceCode;
}
export interface WorkingCopyCommittedEvent {
uiSourceCode: UISourceCode;
content: string;
encoded?: boolean;
}
export interface EventTypes {
[Events.UISourceCodeAdded]: UISourceCode;
[Events.UISourceCodeRemoved]: UISourceCode;
[Events.UISourceCodeRenamed]: UISourceCodeRenamedEvent;
[Events.WorkingCopyChanged]: WorkingCopyChangedEvent;
[Events.WorkingCopyCommitted]: WorkingCopyCommittedEvent;
[Events.WorkingCopyCommittedByUser]: WorkingCopyCommittedEvent;
[Events.ProjectAdded]: Project;
[Events.ProjectRemoved]: Project;
}