Skip to content

Commit dbf253d

Browse files
bmeurerDevtools-frontend LUCI CQ
authored andcommitted
[sources] Show connectable folders inline in the Workspace tab.
Per UX deck[^1], the connectable Automatic Workspace folders must be shown inline in the Workspace tab (of the Sources panel) with a button next to it, that allows the user to connect the folder. This is modeled by adding a transient `Workspace.Workspace.Project` as a placeholder for the automatic file system, while it's not connected yet. The placeholder project automatically disappears when the folder is connected. Tests will be added in a separate CL. [^1]: http://go/chrome-devtools:automatic-workspace-folders-ux Bug: 404170628 Change-Id: I2dd7795e5fd866c605474cd09cc41df3d3d3a2ae Screenshot: http://screen/BpHS5AmayeQ6maG.png Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6440430 Reviewed-by: Alex Rudenko <[email protected]> Commit-Queue: Benedikt Meurer <[email protected]> Auto-Submit: Benedikt Meurer <[email protected]> Commit-Queue: Alex Rudenko <[email protected]>
1 parent fa68d4e commit dbf253d

File tree

14 files changed

+323
-56
lines changed

14 files changed

+323
-56
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,7 @@ grd_files_debug_sources = [
10831083
"front_end/models/logs/RequestResolver.js",
10841084
"front_end/models/persistence/Automapping.js",
10851085
"front_end/models/persistence/AutomaticFileSystemManager.js",
1086+
"front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.js",
10861087
"front_end/models/persistence/EditFileSystemView.js",
10871088
"front_end/models/persistence/FileSystemWorkspaceBinding.js",
10881089
"front_end/models/persistence/IsolatedFileSystem.js",

front_end/entrypoints/main/MainImpl.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,17 @@ export class MainImpl {
509509
targetManager,
510510
});
511511

512-
Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance({
512+
const automaticFileSystemManager = Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance({
513513
forceNew: true,
514514
hostConfig: Root.Runtime.hostConfig,
515515
inspectorFrontendHost: Host.InspectorFrontendHost.InspectorFrontendHostInstance,
516516
projectSettingsModel,
517517
});
518+
Persistence.AutomaticFileSystemWorkspaceBinding.AutomaticFileSystemWorkspaceBinding.instance({
519+
forceNew: true,
520+
automaticFileSystemManager,
521+
workspace: Workspace.Workspace.WorkspaceImpl.instance(),
522+
});
518523

519524
AutofillManager.AutofillManager.AutofillManager.instance();
520525

front_end/models/persistence/AutomaticFileSystemManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class AutomaticFileSystemManager extends Common.ObjectWrapper.ObjectWrapp
100100
if (!automaticFileSystemManagerInstance || forceNew) {
101101
if (!hostConfig || !inspectorFrontendHost || !projectSettingsModel) {
102102
throw new Error(
103-
'Unable to create AutomaticFileSysteManager: ' +
103+
'Unable to create AutomaticFileSystemManager: ' +
104104
'hostConfig, inspectorFrontendHost, and projectSettingsModel must be provided');
105105
}
106106
automaticFileSystemManagerInstance = new AutomaticFileSystemManager(
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import type * as Common from '../../core/common/common.js';
6+
import * as Host from '../../core/host/host.js';
7+
import type * as Platform from '../../core/platform/platform.js';
8+
import type {ContentDataOrError} from '../text_utils/ContentData.js';
9+
import type {SearchMatch} from '../text_utils/ContentProvider.js';
10+
import * as Workspace from '../workspace/workspace.js';
11+
12+
import {type AutomaticFileSystem, type AutomaticFileSystemManager, Events} from './AutomaticFileSystemManager.js';
13+
14+
/**
15+
* @internal
16+
*
17+
* @see AutomaticFileSystemWorkspaceBinding
18+
*/
19+
class ConnectableFileSystemProject implements Workspace.Workspace.Project {
20+
readonly automaticFileSystem: Readonly<AutomaticFileSystem>;
21+
readonly #workspace: Workspace.Workspace.WorkspaceImpl;
22+
23+
constructor(automaticFileSystem: Readonly<AutomaticFileSystem>, workspace: Workspace.Workspace.WorkspaceImpl) {
24+
this.automaticFileSystem = automaticFileSystem;
25+
this.#workspace = workspace;
26+
}
27+
28+
workspace(): Workspace.Workspace.WorkspaceImpl {
29+
return this.#workspace;
30+
}
31+
32+
id(): string {
33+
return `${this.type()}:${this.automaticFileSystem.root}:${this.automaticFileSystem.uuid}`;
34+
}
35+
36+
type(): Workspace.Workspace.projectTypes {
37+
return Workspace.Workspace.projectTypes.ConnectableFileSystem;
38+
}
39+
40+
isServiceProject(): boolean {
41+
return false;
42+
}
43+
44+
displayName(): string {
45+
const {root} = this.automaticFileSystem;
46+
let slash = root.lastIndexOf('/');
47+
if (slash === -1 && Host.Platform.isWin()) {
48+
slash = root.lastIndexOf('\\');
49+
}
50+
return root.substr(slash + 1);
51+
}
52+
53+
async requestMetadata(_uiSourceCode: Workspace.UISourceCode.UISourceCode):
54+
Promise<Workspace.UISourceCode.UISourceCodeMetadata|null> {
55+
throw new Error('Not implemented');
56+
}
57+
58+
async requestFileContent(_uiSourceCode: Workspace.UISourceCode.UISourceCode): Promise<ContentDataOrError> {
59+
throw new Error('Not implemented');
60+
}
61+
62+
canSetFileContent(): boolean {
63+
return false;
64+
}
65+
66+
async setFileContent(_uiSourceCode: Workspace.UISourceCode.UISourceCode, _newContent: string, _isBase64: boolean):
67+
Promise<void> {
68+
throw new Error('Not implemented');
69+
}
70+
71+
fullDisplayName(_uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
72+
throw new Error('Not implemented');
73+
}
74+
75+
mimeType(_uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
76+
throw new Error('Not implemented');
77+
}
78+
79+
canRename(): boolean {
80+
return false;
81+
}
82+
83+
rename(
84+
_uiSourceCode: Workspace.UISourceCode.UISourceCode, _newName: Platform.DevToolsPath.RawPathString,
85+
_callback:
86+
(arg0: boolean, arg1?: string, arg2?: Platform.DevToolsPath.UrlString,
87+
arg3?: Common.ResourceType.ResourceType) => void): void {
88+
throw new Error('Not implemented');
89+
}
90+
91+
excludeFolder(_path: Platform.DevToolsPath.UrlString): void {
92+
throw new Error('Not implemented');
93+
}
94+
95+
canExcludeFolder(_path: Platform.DevToolsPath.EncodedPathString): boolean {
96+
return false;
97+
}
98+
99+
async createFile(
100+
_path: Platform.DevToolsPath.EncodedPathString, _name: string|null, _content: string,
101+
_isBase64?: boolean): Promise<Workspace.UISourceCode.UISourceCode|null> {
102+
throw new Error('Not implemented');
103+
}
104+
105+
canCreateFile(): boolean {
106+
return false;
107+
}
108+
109+
deleteFile(_uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
110+
throw new Error('Not implemented');
111+
}
112+
113+
async deleteDirectoryRecursively(_path: Platform.DevToolsPath.EncodedPathString): Promise<boolean> {
114+
throw new Error('Not implemented');
115+
}
116+
117+
remove(): void {
118+
}
119+
120+
removeUISourceCode(_url: Platform.DevToolsPath.UrlString): void {
121+
throw new Error('Not implemented');
122+
}
123+
124+
async searchInFileContent(
125+
_uiSourceCode: Workspace.UISourceCode.UISourceCode, _query: string, _caseSensitive: boolean,
126+
_isRegex: boolean): Promise<SearchMatch[]> {
127+
return [];
128+
}
129+
130+
async findFilesMatchingSearchRequest(
131+
_searchConfig: Workspace.SearchConfig.SearchConfig,
132+
_filesMatchingFileQuery: Workspace.UISourceCode.UISourceCode[],
133+
_progress: Common.Progress.Progress): Promise<Map<Workspace.UISourceCode.UISourceCode, SearchMatch[]|null>> {
134+
return new Map();
135+
}
136+
137+
indexContent(_progress: Common.Progress.Progress): void {
138+
}
139+
140+
uiSourceCodeForURL(_url: Platform.DevToolsPath.UrlString): Workspace.UISourceCode.UISourceCode|null {
141+
return null;
142+
}
143+
144+
uiSourceCodes(): Iterable<Workspace.UISourceCode.UISourceCode> {
145+
return [];
146+
}
147+
}
148+
149+
let automaticFileSystemWorkspaceBindingInstance: AutomaticFileSystemWorkspaceBinding|undefined;
150+
151+
/**
152+
* Provides a transient workspace `Project` that doesn't contain any `UISourceCode`s,
153+
* and only acts as a placeholder for the automatic file system, while it's not
154+
* connected yet. The placeholder project automatically disappears as soon as
155+
* the automatic file system is connected successfully.
156+
*/
157+
export class AutomaticFileSystemWorkspaceBinding {
158+
readonly #automaticFileSystemManager: AutomaticFileSystemManager;
159+
#automaticFileSystemProject: ConnectableFileSystemProject|null = null;
160+
readonly #workspace: Workspace.Workspace.WorkspaceImpl;
161+
162+
/**
163+
* @internal
164+
*/
165+
private constructor(
166+
automaticFileSystemManager: AutomaticFileSystemManager, workspace: Workspace.Workspace.WorkspaceImpl) {
167+
this.#automaticFileSystemManager = automaticFileSystemManager;
168+
this.#workspace = workspace;
169+
this.#automaticFileSystemManager.addEventListener(
170+
Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystemChanged, this);
171+
this.#automaticFileSystemChanged({data: this.#automaticFileSystemManager.automaticFileSystem});
172+
}
173+
174+
/**
175+
* Yields the `AutomaticFileSystemWorkspaceBinding` singleton.
176+
*
177+
* @returns the singleton.
178+
*/
179+
static instance({forceNew, automaticFileSystemManager, workspace}: {
180+
forceNew: boolean|null,
181+
automaticFileSystemManager: AutomaticFileSystemManager|null,
182+
workspace: Workspace.Workspace.WorkspaceImpl|null,
183+
} = {forceNew: false, automaticFileSystemManager: null, workspace: null}): AutomaticFileSystemWorkspaceBinding {
184+
if (!automaticFileSystemWorkspaceBindingInstance || forceNew) {
185+
if (!automaticFileSystemManager || !workspace) {
186+
throw new Error(
187+
'Unable to create AutomaticFileSystemWorkspaceBinding: ' +
188+
'automaticFileSystemManager and workspace must be provided');
189+
}
190+
automaticFileSystemWorkspaceBindingInstance = new AutomaticFileSystemWorkspaceBinding(
191+
automaticFileSystemManager,
192+
workspace,
193+
);
194+
}
195+
return automaticFileSystemWorkspaceBindingInstance;
196+
}
197+
198+
/**
199+
* Clears the `AutomaticFileSystemWorkspaceBinding` singleton (if any);
200+
*/
201+
static removeInstance(): void {
202+
if (automaticFileSystemWorkspaceBindingInstance) {
203+
automaticFileSystemWorkspaceBindingInstance.#dispose();
204+
automaticFileSystemWorkspaceBindingInstance = undefined;
205+
}
206+
}
207+
208+
#dispose(): void {
209+
if (this.#automaticFileSystemProject) {
210+
this.#workspace.removeProject(this.#automaticFileSystemProject);
211+
}
212+
this.#automaticFileSystemManager.removeEventListener(
213+
Events.AUTOMATIC_FILE_SYSTEM_CHANGED, this.#automaticFileSystemChanged, this);
214+
}
215+
216+
#automaticFileSystemChanged(event: Common.EventTarget.EventTargetEvent<AutomaticFileSystem|null>): void {
217+
const automaticFileSystem = event.data;
218+
if (this.#automaticFileSystemProject !== null) {
219+
if (this.#automaticFileSystemProject.automaticFileSystem === automaticFileSystem) {
220+
return;
221+
}
222+
this.#workspace.removeProject(this.#automaticFileSystemProject);
223+
this.#automaticFileSystemProject = null;
224+
}
225+
if (automaticFileSystem !== null && automaticFileSystem.state !== 'connected') {
226+
this.#automaticFileSystemProject = new ConnectableFileSystemProject(automaticFileSystem, this.#workspace);
227+
this.#workspace.addProject(this.#automaticFileSystemProject);
228+
}
229+
}
230+
}

front_end/models/persistence/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ devtools_module("persistence") {
1919
sources = [
2020
"Automapping.ts",
2121
"AutomaticFileSystemManager.ts",
22+
"AutomaticFileSystemWorkspaceBinding.ts",
2223
"EditFileSystemView.ts",
2324
"FileSystemWorkspaceBinding.ts",
2425
"IsolatedFileSystem.ts",

front_end/models/persistence/persistence-meta.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ const UIStrings = {
5252
*@description Title of a setting under the Persistence category that can be invoked through the Command Menu
5353
*/
5454
disableOverrideNetworkRequests: 'Disable override network requests',
55-
/**
56-
* @description Title of a setting to enable the Automatic Workspace Folders.
57-
*/
58-
enableAutomaticWorkspaceFolders: 'Enable automatic workspace folders',
5955
} as const;
6056
const str_ = i18n.i18n.registerUIStrings('models/persistence/persistence-meta.ts', UIStrings);
6157
const i18nLazyString = i18n.i18n.getLazilyComputedLocalizedString.bind(undefined, str_);
@@ -82,14 +78,6 @@ UI.ViewManager.registerViewExtension({
8278
iconName: 'folder',
8379
});
8480

85-
Common.Settings.registerSettingExtension({
86-
category: Common.Settings.SettingCategory.PERSISTENCE,
87-
title: i18nLazyString(UIStrings.enableAutomaticWorkspaceFolders),
88-
settingName: 'persistence-automatic-workspace-folders',
89-
settingType: Common.Settings.SettingType.BOOLEAN,
90-
defaultValue: false,
91-
});
92-
9381
Common.Settings.registerSettingExtension({
9482
category: Common.Settings.SettingCategory.PERSISTENCE,
9583
title: i18nLazyString(UIStrings.enableLocalOverrides),

front_end/models/persistence/persistence.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import * as Automapping from './Automapping.js';
66
import * as AutomaticFileSystemManager from './AutomaticFileSystemManager.js';
7+
import * as AutomaticFileSystemWorkspaceBinding from './AutomaticFileSystemWorkspaceBinding.js';
78
import * as EditFileSystemView from './EditFileSystemView.js';
89
import * as FileSystemWorkspaceBinding from './FileSystemWorkspaceBinding.js';
910
import * as IsolatedFileSystem from './IsolatedFileSystem.js';
@@ -18,6 +19,7 @@ import * as WorkspaceSettingsTab from './WorkspaceSettingsTab.js';
1819
export {
1920
Automapping,
2021
AutomaticFileSystemManager,
22+
AutomaticFileSystemWorkspaceBinding,
2123
EditFileSystemView,
2224
FileSystemWorkspaceBinding,
2325
IsolatedFileSystem,

front_end/models/workspace/WorkspaceImpl.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export enum projectTypes {
8888
Inspector = 'inspector',
8989
Network = 'network',
9090
FileSystem = 'filesystem',
91+
ConnectableFileSystem = 'connectablefilesystem',
9192
ContentScripts = 'contentscripts',
9293
Service = 'service',
9394
}

front_end/panels/sources/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@ devtools_module("sources") {
8282
"../../panels/snippets:bundle",
8383
"../../panels/utils:bundle",
8484
"../../third_party/diff:bundle",
85+
"../../ui/components/buttons:bundle",
8586
"../../ui/components/floating_button:bundle",
8687
"../../ui/components/icon_button:bundle",
88+
"../../ui/components/spinners:bundle",
8789
"../../ui/components/text_editor:bundle",
8890
"../../ui/legacy:bundle",
8991
"../../ui/legacy/components/color_picker:bundle",

0 commit comments

Comments
 (0)