chrome-devtools-frontend 1.0.1701636 → 1.0.1703574

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/eslint.config.mjs +3 -1
  2. package/front_end/core/common/ParsedURL.ts +5 -1
  3. package/front_end/core/host/UserMetricsEnums.ts +2 -1
  4. package/front_end/core/root/ExperimentNames.ts +1 -0
  5. package/front_end/core/sdk/CSSPropertyParserMatchers.ts +2 -7
  6. package/front_end/core/text_utils/Markdown.ts +73 -0
  7. package/front_end/core/text_utils/text_utils.ts +2 -0
  8. package/front_end/devtools_compatibility.js +2 -1
  9. package/front_end/entrypoints/main/MainImpl.ts +8 -0
  10. package/front_end/generated/InspectorBackendCommands.ts +6 -4
  11. package/front_end/generated/protocol-mapping.d.ts +8 -0
  12. package/front_end/generated/protocol-proxy-api.d.ts +6 -0
  13. package/front_end/generated/protocol.ts +22 -3
  14. package/front_end/models/ai_assistance/AiAgent2.ts +13 -1
  15. package/front_end/models/ai_assistance/AiConversation.ts +1 -1
  16. package/front_end/models/ai_assistance/agents/ContextSelectionAgent.ts +13 -19
  17. package/front_end/models/change_tracker/ChangeTracker.ts +5 -64
  18. package/front_end/models/comment_manager/CD4ABridge.ts +5 -0
  19. package/front_end/models/comment_manager/CommentManager.ts +3 -5
  20. package/front_end/models/comment_manager/CommentThread.ts +30 -13
  21. package/front_end/models/issues_manager/CookieDeprecationMetadataIssue.ts +6 -7
  22. package/front_end/models/issues_manager/DeprecationIssue.ts +1 -1
  23. package/front_end/models/issues_manager/MarkdownIssueDescription.ts +18 -52
  24. package/front_end/models/issues_manager/SRIMessageSignatureIssue.ts +2 -4
  25. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantRead.md +0 -1
  26. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantReadOptOut.md +5 -0
  27. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantSet.md +0 -1
  28. package/front_end/models/issues_manager/descriptions/cookieWarnMetadataGrantSetOptOut.md +5 -0
  29. package/front_end/models/issues_manager/descriptions/deprecation.md +1 -1
  30. package/front_end/models/issues_manager/descriptions/sriValidationFailedIntegrityMismatch.md +2 -0
  31. package/front_end/models/persistence/AutomaticFileSystemWorkspaceBinding.ts +1 -1
  32. package/front_end/models/persistence/NetworkPersistenceManager.ts +14 -2
  33. package/front_end/panels/common/CommentThreadWidget.ts +29 -10
  34. package/front_end/panels/common/CommentsOverlayWidget.ts +50 -14
  35. package/front_end/panels/common/CommentsStatusBarPill.ts +40 -7
  36. package/front_end/panels/common/commentThreadWidget.css +11 -0
  37. package/front_end/panels/common/commentsStatusBarPill.css +13 -0
  38. package/front_end/panels/elements/StylePropertiesSection.ts +10 -0
  39. package/front_end/panels/elements/StylePropertyTreeElement.ts +1 -1
  40. package/front_end/panels/elements/StylesSidebarPane.ts +101 -49
  41. package/front_end/panels/elements/components/CSSQuery.ts +1 -1
  42. package/front_end/panels/elements/components/cssQuery.css +4 -0
  43. package/front_end/panels/elements/elements.ts +0 -1
  44. package/front_end/panels/elements/stylesSidebarPane.css +6 -0
  45. package/front_end/panels/issues/IssueView.ts +5 -1
  46. package/front_end/panels/sources/ResourceOriginPlugin.ts +8 -3
  47. package/front_end/panels/sources/ScopeChainSidebarPane.ts +3 -4
  48. package/front_end/panels/sources/SourcesView.ts +127 -147
  49. package/front_end/panels/sources/TabbedEditorContainer.ts +518 -246
  50. package/front_end/panels/sources/UISourceCodeFrame.ts +9 -0
  51. package/front_end/third_party/chromium/README.chromium +1 -1
  52. package/front_end/third_party/chromium/client-variations/ClientVariations.js +25 -96
  53. package/front_end/third_party/codemirror.next/chunk/angular.js +1 -1
  54. package/front_end/third_party/codemirror.next/chunk/codemirror.js +1 -1
  55. package/front_end/third_party/codemirror.next/chunk/codemirror.js.map +1 -1
  56. package/front_end/third_party/codemirror.next/chunk/cpp.js +1 -1
  57. package/front_end/third_party/codemirror.next/chunk/cpp.js.map +1 -1
  58. package/front_end/third_party/codemirror.next/chunk/java.js +1 -1
  59. package/front_end/third_party/codemirror.next/chunk/java.js.map +1 -1
  60. package/front_end/third_party/codemirror.next/chunk/legacy.js +1 -1
  61. package/front_end/third_party/codemirror.next/chunk/less.js +1 -1
  62. package/front_end/third_party/codemirror.next/chunk/less.js.map +1 -1
  63. package/front_end/third_party/codemirror.next/chunk/markdown.js +1 -1
  64. package/front_end/third_party/codemirror.next/chunk/markdown.js.map +1 -1
  65. package/front_end/third_party/codemirror.next/chunk/php.js +1 -1
  66. package/front_end/third_party/codemirror.next/chunk/php.js.map +1 -1
  67. package/front_end/third_party/codemirror.next/chunk/python.js +1 -1
  68. package/front_end/third_party/codemirror.next/chunk/python.js.map +1 -1
  69. package/front_end/third_party/codemirror.next/chunk/sass.js +1 -1
  70. package/front_end/third_party/codemirror.next/chunk/sass.js.map +1 -1
  71. package/front_end/third_party/codemirror.next/chunk/svelte.js +1 -1
  72. package/front_end/third_party/codemirror.next/chunk/svelte.js.map +1 -1
  73. package/front_end/third_party/codemirror.next/chunk/vue.js +1 -1
  74. package/front_end/third_party/codemirror.next/chunk/wast.js +1 -1
  75. package/front_end/third_party/codemirror.next/chunk/wast.js.map +1 -1
  76. package/front_end/third_party/codemirror.next/chunk/xml.js +1 -1
  77. package/front_end/third_party/codemirror.next/chunk/xml.js.map +1 -1
  78. package/front_end/third_party/codemirror.next/codemirror.next.js +1 -1
  79. package/front_end/third_party/codemirror.next/package.json +4 -1
  80. package/front_end/third_party/codemirror.next/rebuild.sh +1 -1
  81. package/front_end/ui/comments/CommentOverlayManager.ts +11 -10
  82. package/front_end/ui/components/markdown_view/MarkdownPlaceholderLitRenderer.ts +59 -0
  83. package/front_end/ui/components/markdown_view/markdown_view.ts +2 -0
  84. package/front_end/ui/legacy/TabbedPane.ts +51 -40
  85. package/front_end/ui/legacy/Treeoutline.ts +28 -19
  86. package/front_end/ui/legacy/UIUtils.ts +1 -1
  87. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +424 -570
  88. package/front_end/ui/legacy/components/object_ui/objectPropertiesSection.css +2 -27
  89. package/front_end/ui/visual_logging/KnownContextValues.ts +1 -0
  90. package/mcp/mcp.ts +1 -0
  91. package/package.json +2 -1
@@ -2,6 +2,9 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
 
5
+ import '../../ui/components/tooltips/tooltips.js';
6
+ import '../../ui/kit/kit.js';
7
+
5
8
  import * as Common from '../../core/common/common.js';
6
9
  import * as Host from '../../core/host/host.js';
7
10
  import * as i18n from '../../core/i18n/i18n.js';
@@ -10,12 +13,10 @@ import * as TextUtils from '../../core/text_utils/text_utils.js';
10
13
  import * as Persistence from '../../models/persistence/persistence.js';
11
14
  import * as Workspace from '../../models/workspace/workspace.js';
12
15
  import type * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js';
13
- import * as Tooltips from '../../ui/components/tooltips/tooltips.js';
14
16
  import * as uiI18n from '../../ui/i18n/i18n.js';
15
- import {Icon, Link} from '../../ui/kit/kit.js';
16
17
  import * as SourceFrame from '../../ui/legacy/components/source_frame/source_frame.js';
17
18
  import * as UI from '../../ui/legacy/legacy.js';
18
- import {html, render} from '../../ui/lit/lit.js';
19
+ import {Directives, html, type LitTemplate, nothing, render} from '../../ui/lit/lit.js';
19
20
  import * as VisualLogging from '../../ui/visual_logging/visual_logging.js';
20
21
  import * as PanelCommon from '../common/common.js';
21
22
  import * as Snippets from '../snippets/snippets.js';
@@ -75,6 +76,8 @@ const UIStrings = {
75
76
  } as const;
76
77
  const str_ = i18n.i18n.registerUIStrings('panels/sources/TabbedEditorContainer.ts', UIStrings);
77
78
  const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
79
+ const {repeat} = Directives;
80
+ const {widget} = UI.Widget;
78
81
 
79
82
  const enum SourceViewType {
80
83
  IMAGE_VIEW = 'ImageView',
@@ -84,6 +87,191 @@ const enum SourceViewType {
84
87
  }
85
88
  const HEADER_OVERRIDES_FILENAME = '.headers';
86
89
 
90
+ interface TabInfo {
91
+ tabId: string;
92
+ title: string;
93
+ tooltip: string;
94
+ uiSourceCode: Workspace.UISourceCode.UISourceCode;
95
+ isCloseable: boolean;
96
+ widget?: UI.Widget.Widget;
97
+ hasLoadError: boolean;
98
+ hasUnsavedCommittedChanges: boolean;
99
+ disconnectedAutomaticFileSystemRoot?: string;
100
+ icon?: LitTemplate;
101
+ }
102
+
103
+ export interface TabbedEditorViewInput {
104
+ openTabs: TabInfo[];
105
+ activeTabId?: string;
106
+ leftToolbarItems: Array<UI.Toolbar.ToolbarItem|LitTemplate>;
107
+ rightToolbarItems: Array<UI.Toolbar.ToolbarItem|LitTemplate>;
108
+ tabDelegate: UI.TabbedPane.TabbedPaneTabDelegate;
109
+ shortcuts: Array<{
110
+ description: Platform.UIString.LocalizedString,
111
+ onClick: () => void,
112
+ keys: string[],
113
+ }>;
114
+ onAddFileSystemClicked: () => void;
115
+ onConnectAutomaticFileSystem: (e: Event) => void;
116
+ onClose: (e: Event) => void;
117
+ onTabOrderChanged: (e: Event) => void;
118
+ onSelect: (e: Event) => void;
119
+ }
120
+
121
+ export type View = (input: TabbedEditorViewInput, output: undefined, target: HTMLElement) => void;
122
+
123
+ const UI_SOURCE_CODE_WIDGET_MAP = new WeakMap<Workspace.UISourceCode.UISourceCode, UI.Widget.Widget>();
124
+
125
+ function getOrCreateSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode,
126
+ onCreate: (view: UI.Widget.Widget) => void): UI.Widget.Widget {
127
+ const existing = UI_SOURCE_CODE_WIDGET_MAP.get(uiSourceCode);
128
+ if (existing) {
129
+ return existing;
130
+ }
131
+
132
+ let sourceView: UI.Widget.Widget;
133
+ const contentType = uiSourceCode.contentType();
134
+ if (contentType === Common.ResourceType.resourceTypes.Image || uiSourceCode.mimeType().startsWith('image/')) {
135
+ sourceView = new SourceFrame.ImageView.ImageView(uiSourceCode.mimeType(), uiSourceCode);
136
+ } else if (contentType === Common.ResourceType.resourceTypes.Font || uiSourceCode.mimeType().includes('font')) {
137
+ sourceView = new SourceFrame.FontView.FontView(uiSourceCode.mimeType(), uiSourceCode);
138
+ } else if (uiSourceCode.name() === Persistence.NetworkPersistenceManager.HEADERS_FILENAME) {
139
+ sourceView = new Components.HeadersView.HeadersView(uiSourceCode);
140
+ } else {
141
+ sourceView = new UISourceCodeFrame(uiSourceCode);
142
+ }
143
+
144
+ UI_SOURCE_CODE_WIDGET_MAP.set(uiSourceCode, sourceView);
145
+ onCreate(sourceView);
146
+ return sourceView;
147
+ }
148
+
149
+ function recycleUISourceCodeFrame(sourceFrame: UISourceCodeFrame,
150
+ uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
151
+ UI_SOURCE_CODE_WIDGET_MAP.delete(sourceFrame.uiSourceCode());
152
+ sourceFrame.setUISourceCode(uiSourceCode);
153
+ UI_SOURCE_CODE_WIDGET_MAP.set(uiSourceCode, sourceFrame);
154
+ }
155
+
156
+ function getViewByUISourceCode(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget|undefined {
157
+ return UI_SOURCE_CODE_WIDGET_MAP.get(uiSourceCode);
158
+ }
159
+
160
+ function removeSourceViewCache(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget|undefined {
161
+ const view = UI_SOURCE_CODE_WIDGET_MAP.get(uiSourceCode);
162
+ if (view) {
163
+ UI_SOURCE_CODE_WIDGET_MAP.delete(uiSourceCode);
164
+ }
165
+ return view;
166
+ }
167
+ function renderPlaceholder(input: TabbedEditorViewInput): LitTemplate {
168
+ // clang-format off
169
+ return html`
170
+ <div class="sources-placeholder">
171
+ <div class="tabbed-pane-placeholder-row workspace">
172
+ <span class="icon-container">
173
+ <devtools-icon name="sync" class="sync-icon"></devtools-icon>
174
+ </span>
175
+ <span>
176
+ ${i18nString(UIStrings.workspaceDropInAFolderToSyncSources)}
177
+ <button @click=${input.onAddFileSystemClicked}>${i18nString(UIStrings.selectFolder)}</button>
178
+ </span>
179
+ </div>
180
+ <div class="shortcuts-list tabbed-pane-placeholder-row" role="list"
181
+ aria-label=${i18nString(UIStrings.sourceViewActions)}>
182
+ ${input.shortcuts.map(shortcut => !shortcut.keys.length
183
+ ? html`<div class="shortcut-line" role="listitem"></div>`
184
+ : html`<div class="shortcut-line" role="listitem">
185
+ <button @click=${shortcut.onClick}>${shortcut.description}</button>
186
+ <span class="shortcuts">
187
+ ${shortcut.keys.map(key => html`
188
+ <span class="keybinds-key"><span>${key}</span></span>
189
+ `)}
190
+ </span>
191
+ </div>`)}
192
+ </div>
193
+ </div>`;
194
+ // clang-format on
195
+ }
196
+
197
+ function renderTabIcon(tab: TabInfo): LitTemplate {
198
+ if (tab.hasLoadError) {
199
+ // clang-format off
200
+ return html`
201
+ <span slot="icon">
202
+ <devtools-icon class="small" name="cross-circle-filled"
203
+ title=${i18nString(UIStrings.unableToLoadThisContent)}>
204
+ </devtools-icon>
205
+ </span>`;
206
+ // clang-format on
207
+ }
208
+ if (tab.icon) {
209
+ return html`<span slot="icon">${tab.icon}</span>`;
210
+ }
211
+ return nothing;
212
+ }
213
+
214
+ function renderTabSuffix(tab: TabInfo, input: TabbedEditorViewInput): LitTemplate {
215
+ if (!tab.hasUnsavedCommittedChanges) {
216
+ return nothing;
217
+ }
218
+ const tooltipId = `tab-tooltip-${tab.tabId}`;
219
+ // clang-format off
220
+ return html`
221
+ <span slot="suffix">
222
+ <div>
223
+ <devtools-icon name="warning-filled" class="small" aria-describedby=${tooltipId}></devtools-icon>
224
+ <devtools-tooltip id=${tooltipId} variant="rich">
225
+ ${tab.disconnectedAutomaticFileSystemRoot !== undefined
226
+ ? uiI18n.getFormatLocalizedStringTemplate(
227
+ str_, UIStrings.changesWereNotSavedToFileSystemToSaveAddFolderToWorkspace, {
228
+ PH1: html`<devtools-link class="devtools-link" @click=${input.onConnectAutomaticFileSystem}>${
229
+ tab.disconnectedAutomaticFileSystemRoot}</devtools-link>`,
230
+ })
231
+ : uiI18n.getFormatLocalizedStringTemplate(
232
+ str_, UIStrings.changesWereNotSavedToFileSystemToSaveSetUpYourWorkspace, {
233
+ PH1: html`<devtools-link href="https://developer.chrome.com/docs/devtools/workspaces/">Workspace</devtools-link>`,
234
+ })}
235
+ </devtools-tooltip>
236
+ </div>
237
+ </span>`;
238
+ // clang-format on
239
+ }
240
+
241
+ export const DEFAULT_VIEW: View = (input, _output, target) => {
242
+ // clang-format off
243
+ render(html`
244
+ <devtools-tabbed-pane
245
+ class="flex-auto vbox"
246
+ .closeableTabs=${true}
247
+ .allowTabReorder=${true}
248
+ .automaticReorder=${true}
249
+ .tabDelegate=${input.tabDelegate}
250
+ .headerJslog=${`${VisualLogging.toolbar('top').track({keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space'})}`}
251
+ .placeholder=${renderPlaceholder(input)}
252
+ @close=${input.onClose}
253
+ @taborderchanged=${input.onTabOrderChanged}
254
+ @select=${input.onSelect}
255
+ >
256
+ <devtools-toolbar class="tabbed-pane-left-toolbar" slot="left">
257
+ ${input.leftToolbarItems.map(item => item instanceof UI.Toolbar.ToolbarItem ? item.element : item)}
258
+ </devtools-toolbar>
259
+ <devtools-toolbar class="tabbed-pane-right-toolbar" slot="right">
260
+ ${input.rightToolbarItems.map(item => item instanceof UI.Toolbar.ToolbarItem ? item.element : item)}
261
+ </devtools-toolbar>
262
+ ${repeat(input.openTabs, tab => tab.tabId, tab => html`
263
+ <div id=${tab.tabId}
264
+ title=${tab.title}
265
+ ?closeable=${tab.isCloseable}
266
+ ?selected=${input.activeTabId === tab.tabId}
267
+ style="display: flex; flex: auto;">
268
+ ${renderTabIcon(tab)}
269
+ ${renderTabSuffix(tab, input)}
270
+ ${tab.widget ? html`${widget(UI.Widget.WrapperWidget, {widget: tab.widget})}` : nothing}
271
+ </div>`)}
272
+ </devtools-tabbed-pane>`, target);
273
+ // clang-format on
274
+ };
87
275
  let tabId = 0;
88
276
 
89
277
  const TabbedEditorContainerBase: Common.ObjectWrapper.EventMixin<EventTypes, typeof UI.Widget.VBox> =
@@ -92,17 +280,174 @@ const TabbedEditorContainerBase: Common.ObjectWrapper.EventMixin<EventTypes, typ
92
280
  );
93
281
 
94
282
  export class TabbedEditorContainer extends TabbedEditorContainerBase {
283
+ override focus(): void {
284
+ if (this.visibleView) {
285
+ this.visibleView.focus();
286
+ }
287
+ }
288
+
289
+ #scheduleUpdate(): void {
290
+ this.performUpdate();
291
+ }
292
+
293
+ override performUpdate(): void {
294
+ if (false) {
295
+ return;
296
+ }
297
+ const shortcuts = [
298
+ {actionId: 'quick-open.show', description: i18nString(UIStrings.openFile)},
299
+ {actionId: 'quick-open.show-command-menu', description: i18nString(UIStrings.runCommand)},
300
+ ];
301
+ const separator = Host.Platform.isMac() ? '\u2004' : ' + ';
302
+ const shortcutElements = shortcuts.map(shortcut => {
303
+ const shortcutKeys = UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction(shortcut.actionId);
304
+ if (!shortcutKeys?.[0]) {
305
+ return {
306
+ description: shortcut.description,
307
+ onClick: () => {},
308
+ keys: [],
309
+ };
310
+ }
311
+ const action = UI.ActionRegistry.ActionRegistry.instance().getAction(shortcut.actionId);
312
+ const keys = shortcutKeys[0].descriptors.flatMap(descriptor => descriptor.name.split(separator));
313
+ return {
314
+ description: shortcut.description,
315
+ onClick: () => {
316
+ void action.execute();
317
+ },
318
+ keys,
319
+ };
320
+ });
321
+
322
+ const input: TabbedEditorViewInput = {
323
+ openTabs: [...this.files.entries()].map(([tabId, uiSourceCode]) => {
324
+ const hasLoadError = Boolean(uiSourceCode.loadError()) || this.#loadErrorFiles.has(uiSourceCode);
325
+ const hasUnsavedCommittedChanges = !hasLoadError &&
326
+ Persistence.Persistence.PersistenceImpl.instance().hasUnsavedCommittedChanges(uiSourceCode);
327
+ let disconnectedAutomaticFileSystemRoot: string|undefined;
328
+ if (hasUnsavedCommittedChanges) {
329
+ const {automaticFileSystem} = Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance();
330
+ if (automaticFileSystem?.state === 'disconnected') {
331
+ disconnectedAutomaticFileSystemRoot = Common.ParsedURL.ParsedURL.extractName(automaticFileSystem.root);
332
+ }
333
+ }
334
+ const icon = !hasLoadError ?
335
+ (PanelCommon.PersistenceUtils.PersistenceUtils.iconForUISourceCode(uiSourceCode) ?? undefined) :
336
+ undefined;
337
+ return {
338
+ tabId,
339
+ title: this.titleForFile(uiSourceCode),
340
+ tooltip: this.tooltipForFile(uiSourceCode),
341
+ uiSourceCode,
342
+ isCloseable: true,
343
+ hasLoadError,
344
+ hasUnsavedCommittedChanges,
345
+ disconnectedAutomaticFileSystemRoot,
346
+ icon,
347
+ widget: (this.#currentFile === uiSourceCode) ? this.getOrCreateSourceView(uiSourceCode) :
348
+ this.getCreatedSourceView(uiSourceCode),
349
+ };
350
+ }),
351
+ activeTabId: this.#currentFile ? this.tabIds.get(this.#currentFile) : undefined,
352
+ leftToolbarItems: this.#leftToolbarItems,
353
+ rightToolbarItems: this.#rightToolbarItems,
354
+ tabDelegate: this.#tabDelegate,
355
+ shortcuts: shortcutElements,
356
+ onAddFileSystemClicked: () => {
357
+ void this.#addFileSystemClicked();
358
+ },
359
+ onConnectAutomaticFileSystem: async (event: Event) => {
360
+ event.consume();
361
+ await UI.ViewManager.ViewManager.instance().showView('navigator-files');
362
+ await Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance().connectAutomaticFileSystem(
363
+ /* addIfMissing= */ true);
364
+ },
365
+ onClose: (e: Event) => {
366
+ this.tabClosed((e as CustomEvent<{tabId: string}>).detail.tabId,
367
+ (e as CustomEvent<{isUserGesture?: boolean}>).detail.isUserGesture);
368
+ this.#scheduleUpdate();
369
+ },
370
+ onTabOrderChanged: (e: Event) => {
371
+ const tabIds = (e as CustomEvent<{tabIds: string[]}>).detail.tabIds;
372
+ const newFiles = new Map<string, Workspace.UISourceCode.UISourceCode>();
373
+ for (const id of tabIds) {
374
+ if (this.files.has(id)) {
375
+ const file = this.files.get(id);
376
+ if (file) {
377
+ newFiles.set(id, file);
378
+ }
379
+ }
380
+ }
381
+ this.files = newFiles;
382
+ this.#scheduleUpdate();
383
+ },
384
+ onSelect: (e: Event) => {
385
+ this.tabSelected((e as CustomEvent<{tabId: string}>).detail.tabId,
386
+ (e as CustomEvent<{isUserGesture?: boolean}>).detail.isUserGesture);
387
+ this.#scheduleUpdate();
388
+ },
389
+ };
390
+ this.#view(input, undefined, this.contentElement);
391
+ }
95
392
  #historyManager!: EditingLocationHistoryManager;
96
393
  set historyManager(historyManager: EditingLocationHistoryManager) {
97
394
  this.#historyManager = historyManager;
98
395
  }
99
- private readonly sourceViewByUISourceCode = new Map<Workspace.UISourceCode.UISourceCode, UI.Widget.Widget>();
100
- private readonly tabbedPane: UI.TabbedPane.TabbedPane;
396
+
397
+ #leftToolbarItems: Array<UI.Toolbar.ToolbarItem|LitTemplate> = [];
398
+ set leftToolbarItems(items: Array<UI.Toolbar.ToolbarItem|LitTemplate>) {
399
+ if (this.#leftToolbarItems === items) {
400
+ return;
401
+ }
402
+ this.#leftToolbarItems = items;
403
+ this.#scheduleUpdate();
404
+ }
405
+
406
+ #rightToolbarItems: Array<UI.Toolbar.ToolbarItem|LitTemplate> = [];
407
+ set rightToolbarItems(items: Array<UI.Toolbar.ToolbarItem|LitTemplate>) {
408
+ if (this.#rightToolbarItems === items) {
409
+ return;
410
+ }
411
+ this.#rightToolbarItems = items;
412
+ this.#scheduleUpdate();
413
+ }
414
+
415
+ readonly #syncedUISourceCodes = new Set<Workspace.UISourceCode.UISourceCode>();
416
+ set uiSourceCodes(uiSourceCodes: ReadonlySet<Workspace.UISourceCode.UISourceCode>) {
417
+ const removed: Workspace.UISourceCode.UISourceCode[] = [];
418
+ for (const existing of this.#syncedUISourceCodes) {
419
+ if (!uiSourceCodes.has(existing)) {
420
+ removed.push(existing);
421
+ this.#syncedUISourceCodes.delete(existing);
422
+ }
423
+ }
424
+ if (removed.length > 0) {
425
+ this.removeUISourceCodes(removed);
426
+ }
427
+ UI.UIUtils.startBatchUpdate();
428
+ for (const uiSourceCode of uiSourceCodes) {
429
+ if (!this.#syncedUISourceCodes.has(uiSourceCode)) {
430
+ this.#syncedUISourceCodes.add(uiSourceCode);
431
+ this.addUISourceCode(uiSourceCode);
432
+ }
433
+ }
434
+ UI.UIUtils.endBatchUpdate();
435
+ }
436
+
437
+ onEditorSelected?: (event: EditorSelectedEvent) => void;
438
+ onEditorClosed?: (uiSourceCode: Workspace.UISourceCode.UISourceCode) => void;
439
+
440
+ readonly #view: View;
441
+ readonly #tabDelegate: EditorContainerTabDelegate;
101
442
  private tabIds: Map<Workspace.UISourceCode.UISourceCode, string>;
102
- private readonly files: Map<string, Workspace.UISourceCode.UISourceCode>;
443
+ private files: Map<string, Workspace.UISourceCode.UISourceCode>;
444
+ readonly #loadErrorFiles = new Set<Workspace.UISourceCode.UISourceCode>();
103
445
  #previouslyViewedFilesSetting!: Common.Settings.Setting<SerializedHistoryItem[]>;
104
446
  history!: History;
105
447
  set previouslyViewedFilesSetting(setting: Common.Settings.Setting<SerializedHistoryItem[]>) {
448
+ if (this.#previouslyViewedFilesSetting === setting) {
449
+ return;
450
+ }
106
451
  this.#previouslyViewedFilesSetting = setting;
107
452
  this.history = History.fromObject(this.#previouslyViewedFilesSetting.get());
108
453
  }
@@ -115,47 +460,42 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
115
460
  private currentView!: UI.Widget.Widget|null;
116
461
  private scrollTimer?: number;
117
462
  private reentrantShow: boolean;
118
- constructor(element?: HTMLElement) {
463
+ constructor(element?: HTMLElement, view: View = DEFAULT_VIEW) {
119
464
  super(element);
120
-
121
- this.tabbedPane = new UI.TabbedPane.TabbedPane();
122
- // eslint-disable-next-line @devtools/no-imperative-dom-api
123
- this.tabbedPane.show(this.contentElement);
124
- // eslint-disable-next-line @devtools/no-imperative-dom-api
125
- const placeholderElement = document.createElement('div');
126
- placeholderElement.classList.add('sources-placeholder');
127
- this.tabbedPane.setPlaceholderElement(placeholderElement);
128
- this.#renderPlaceholder(placeholderElement as HTMLElement);
129
- this.tabbedPane.setTabDelegate(new EditorContainerTabDelegate(this));
130
-
131
- this.tabbedPane.setCloseableTabs(true);
132
- this.tabbedPane.setAllowTabReorder(true, true);
133
-
134
- this.tabbedPane.addEventListener(UI.TabbedPane.Events.TabClosed, this.tabClosed, this);
135
- this.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this.tabSelected, this);
136
-
137
- this.tabbedPane.headerElement().setAttribute(
138
- 'jslog',
139
- `${VisualLogging.toolbar('top').track({keydown: 'ArrowUp|ArrowLeft|ArrowDown|ArrowRight|Enter|Space'})}`);
140
-
141
- Persistence.Persistence.PersistenceImpl.instance().addEventListener(
142
- Persistence.Persistence.Events.BindingCreated, this.onBindingCreated, this);
143
- Persistence.Persistence.PersistenceImpl.instance().addEventListener(
144
- Persistence.Persistence.Events.BindingRemoved, this.onBindingRemoved, this);
145
- Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().addEventListener(
146
- Persistence.NetworkPersistenceManager.Events.REQUEST_FOR_HEADER_OVERRIDES_FILE_CHANGED,
147
- this.#onRequestsForHeaderOverridesFileChanged, this);
465
+ this.#view = view;
466
+ this.#tabDelegate = new EditorContainerTabDelegate(this);
148
467
 
149
468
  this.tabIds = new Map();
150
469
  this.files = new Map();
151
-
152
470
  this.uriToUISourceCode = new Map();
153
471
  this.idToUISourceCode = new Map();
154
472
  this.reentrantShow = false;
473
+
474
+ this.performUpdate();
475
+
476
+ Persistence.Persistence.PersistenceImpl.instance().addEventListener(Persistence.Persistence.Events.BindingCreated,
477
+ this.onBindingCreated, this);
478
+ Persistence.Persistence.PersistenceImpl.instance().addEventListener(Persistence.Persistence.Events.BindingRemoved,
479
+ this.onBindingRemoved, this);
480
+ Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().addEventListener(
481
+ Persistence.NetworkPersistenceManager.Events.REQUEST_FOR_HEADER_OVERRIDES_FILE_CHANGED,
482
+ this.#onRequestsForHeaderOverridesFileChanged, this);
483
+ }
484
+ #tabsHistory: string[] = [];
485
+ #appendHistory(tabId: string): void {
486
+ this.#tabsHistory.unshift(tabId);
487
+ }
488
+
489
+ get #tabbedPane(): UI.TabbedPane.TabbedPaneElement|null {
490
+ return this.contentElement.querySelector('devtools-tabbed-pane');
155
491
  }
156
492
 
157
- get tabbedPaneForTesting(): UI.TabbedPane.TabbedPane {
158
- return this.tabbedPane;
493
+ get tabbedPane(): UI.TabbedPane.TabbedPaneElement {
494
+ return this.#tabbedPane as UI.TabbedPane.TabbedPaneElement;
495
+ }
496
+
497
+ get tabbedPaneForTesting(): UI.TabbedPane.TabbedPaneElement {
498
+ return this.#tabbedPane as UI.TabbedPane.TabbedPaneElement;
159
499
  }
160
500
 
161
501
  private onBindingCreated(event: Common.EventTarget.EventTargetEvent<Persistence.Persistence.PersistenceBinding>):
@@ -177,21 +517,24 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
177
517
  }
178
518
 
179
519
  if (!fileSystemTabId) {
180
- const networkView = this.tabbedPane.tabView(networkTabId);
181
- const tabIndex = this.tabbedPane.tabIndex(networkTabId);
520
+ const networkView: UI.Widget.Widget|null = this.viewForFile(binding.network);
521
+ const tabIndex = Array.from(this.files.keys()).indexOf(networkTabId);
182
522
  if (networkView instanceof UISourceCodeFrame) {
183
523
  this.recycleUISourceCodeFrame(networkView, binding.fileSystem);
184
- fileSystemTabId = this.appendFileTab(binding.fileSystem, false, tabIndex, networkView);
524
+ fileSystemTabId = this.appendFileTab(binding.fileSystem, tabIndex, true);
185
525
  } else {
186
- fileSystemTabId = this.appendFileTab(binding.fileSystem, false, tabIndex);
187
- const fileSystemTabView = (this.tabbedPane.tabView(fileSystemTabId) as UI.Widget.Widget);
188
- this.restoreEditorProperties(fileSystemTabView, currentSelectionRange, currentScrollLineNumber);
526
+ fileSystemTabId = this.appendFileTab(binding.fileSystem, tabIndex);
527
+ const fileSystemTabView = this.viewForFile(binding.fileSystem);
528
+ if (fileSystemTabView) {
529
+ this.restoreEditorProperties(fileSystemTabView, currentSelectionRange, currentScrollLineNumber);
530
+ }
189
531
  }
190
532
  }
191
533
 
192
534
  this.closeTabs([networkTabId], true);
193
535
  if (wasSelectedInNetwork) {
194
- this.tabbedPane.selectTab(fileSystemTabId, false);
536
+ this.#currentFile = this.files.get(fileSystemTabId) || null;
537
+ this.#scheduleUpdate();
195
538
  }
196
539
 
197
540
  this.updateHistory();
@@ -209,19 +552,11 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
209
552
  }
210
553
 
211
554
  get visibleView(): UI.Widget.Widget|null {
212
- return this.tabbedPane.visibleView as UI.Widget.Widget | null;
555
+ return this.#currentFile ? getViewByUISourceCode(this.#currentFile) || null : null;
213
556
  }
214
557
 
215
558
  fileViews(): UI.Widget.Widget[] {
216
- return this.tabbedPane.tabViews() as UI.Widget.Widget[];
217
- }
218
-
219
- leftToolbar(): UI.Toolbar.Toolbar {
220
- return this.tabbedPane.leftToolbar();
221
- }
222
-
223
- rightToolbar(): UI.Toolbar.Toolbar {
224
- return this.tabbedPane.rightToolbar();
559
+ return Array.from(this.files.values()).map(getViewByUISourceCode).filter(Boolean) as UI.Widget.Widget[];
225
560
  }
226
561
 
227
562
  showFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
@@ -250,11 +585,13 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
250
585
  }
251
586
 
252
587
  closeAllFiles(): void {
253
- this.closeTabs(this.tabbedPane.tabIds());
588
+ this.closeTabs(Array.from(this.files.keys()));
254
589
  }
255
590
 
256
591
  detachEditors(): void {
257
- this.tabbedPane.detachChildWidgets();
592
+ for (const view of this.fileViews()) {
593
+ view.detach();
594
+ }
258
595
  }
259
596
 
260
597
  historyUISourceCodes(): Workspace.UISourceCode.UISourceCode[] {
@@ -269,11 +606,37 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
269
606
  }
270
607
 
271
608
  selectNextTab(): void {
272
- this.tabbedPane.selectNextTab();
609
+ const tabIds = Array.from(this.files.keys());
610
+ if (tabIds.length === 0 || !this.#currentFile) {
611
+ return;
612
+ }
613
+ const currentTabId = this.tabIds.get(this.#currentFile);
614
+ if (!currentTabId) {
615
+ return;
616
+ }
617
+ const index = tabIds.indexOf(currentTabId);
618
+ const nextIndex = (index + 1) % tabIds.length;
619
+ const nextFile = this.files.get(tabIds[nextIndex]);
620
+ if (nextFile) {
621
+ this.#showFile(nextFile, false);
622
+ }
273
623
  }
274
624
 
275
625
  selectPrevTab(): void {
276
- this.tabbedPane.selectPrevTab();
626
+ const tabIds = Array.from(this.files.keys());
627
+ if (tabIds.length === 0 || !this.#currentFile) {
628
+ return;
629
+ }
630
+ const currentTabId = this.tabIds.get(this.#currentFile);
631
+ if (!currentTabId) {
632
+ return;
633
+ }
634
+ const index = tabIds.indexOf(currentTabId);
635
+ const prevIndex = (index - 1 + tabIds.length) % tabIds.length;
636
+ const prevFile = this.files.get(tabIds[prevIndex]);
637
+ if (prevFile) {
638
+ this.#showFile(prevFile, false);
639
+ }
277
640
  }
278
641
 
279
642
  private addViewListeners(): void {
@@ -311,8 +674,8 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
311
674
  if (update.docChanged || update.selectionSet) {
312
675
  const {main} = update.state.selection;
313
676
  const lineFrom = update.state.doc.lineAt(main.from), lineTo = update.state.doc.lineAt(main.to);
314
- const range = new TextUtils.TextRange.TextRange(
315
- lineFrom.number - 1, main.from - lineFrom.from, lineTo.number - 1, main.to - lineTo.from);
677
+ const range = new TextUtils.TextRange.TextRange(lineFrom.number - 1, main.from - lineFrom.from, lineTo.number - 1,
678
+ main.to - lineTo.from);
316
679
  if (this.#currentFile) {
317
680
  this.history.updateSelectionRange(historyItemKey(this.#currentFile), range);
318
681
  }
@@ -342,9 +705,12 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
342
705
  // Selecting the tab may cause showFile to be called again, but with the canonical source code,
343
706
  // which is not what we want, so we prevent reentrant calls.
344
707
  this.reentrantShow = true;
345
- const tabId = this.tabIds.get(canonicalSourceCode) || this.appendFileTab(canonicalSourceCode, userGesture);
708
+ const tabId = this.tabIds.get(canonicalSourceCode) || this.appendFileTab(canonicalSourceCode);
709
+ this.#appendHistory(tabId);
346
710
 
347
- this.tabbedPane.selectTab(tabId, userGesture);
711
+ this.#scheduleUpdate();
712
+ // Force TabbedPaneElement to sync its tabs synchronously to avoid layout races in E2E tests.
713
+ this.#tabbedPane?.tabs;
348
714
  } finally {
349
715
  this.reentrantShow = false;
350
716
  }
@@ -354,6 +720,9 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
354
720
  }
355
721
 
356
722
  const previousView = this.currentView;
723
+ if (uiSourceCode) {
724
+ this.getOrCreateSourceView(uiSourceCode);
725
+ }
357
726
  this.currentView = this.visibleView;
358
727
  this.addViewListeners();
359
728
 
@@ -375,6 +744,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
375
744
  userGesture,
376
745
  };
377
746
  this.dispatchEventToListeners(Events.EDITOR_SELECTED, eventData);
747
+ this.onEditorSelected?.(eventData);
378
748
  }
379
749
 
380
750
  private titleForFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): string {
@@ -396,9 +766,13 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
396
766
  if (!shouldPrompt || confirm(i18nString(UIStrings.areYouSureYouWantToCloseUnsaved, {PH1: uiSourceCode.name()}))) {
397
767
  uiSourceCode.resetWorkingCopy();
398
768
  if (nextTabId) {
399
- this.tabbedPane.selectTab(nextTabId, true);
769
+ const nextFile = this.files.get(nextTabId);
770
+ if (nextFile) {
771
+ this.#showFile(nextFile, false);
772
+ }
400
773
  }
401
- this.tabbedPane.closeTab(id, true);
774
+ this.tabClosed(id, true);
775
+ this.#scheduleUpdate();
402
776
  return true;
403
777
  }
404
778
  return false;
@@ -419,9 +793,13 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
419
793
  }
420
794
  }
421
795
  if (dirtyTabs.length) {
422
- this.tabbedPane.selectTab(dirtyTabs[0], true);
796
+ const dirtyFile = this.files.get(dirtyTabs[0]);
797
+ if (dirtyFile) {
798
+ this.#showFile(dirtyFile, false);
799
+ }
423
800
  }
424
- this.tabbedPane.closeTabs(cleanTabs, true);
801
+ cleanTabs.forEach(id => this.tabClosed(id, true));
802
+ this.#scheduleUpdate();
425
803
  for (let i = 0; i < dirtyTabs.length; ++i) {
426
804
  const nextTabId = i + 1 < dirtyTabs.length ? dirtyTabs[i + 1] : null;
427
805
  if (!this.maybeCloseTab(dirtyTabs[i], nextTabId)) {
@@ -470,7 +848,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
470
848
  }
471
849
 
472
850
  if (!this.tabIds.has(uiSourceCode)) {
473
- this.appendFileTab(uiSourceCode, false);
851
+ this.appendFileTab(uiSourceCode);
474
852
  }
475
853
 
476
854
  // Select tab if this file was the last to be shown.
@@ -500,16 +878,22 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
500
878
  const tabId = this.tabIds.get(uiSourceCode);
501
879
  if (tabId) {
502
880
  tabIds.push(tabId);
881
+ } else {
882
+ this.removeSourceFrame(uiSourceCode);
503
883
  }
504
- if (this.uriToUISourceCode.get(uiSourceCode.url()) === uiSourceCode) {
505
- this.uriToUISourceCode.delete(uiSourceCode.url());
884
+ for (const [k, v] of this.uriToUISourceCode) {
885
+ if (v === uiSourceCode) {
886
+ this.uriToUISourceCode.delete(k);
887
+ }
506
888
  }
507
- if (this.idToUISourceCode.get(uiSourceCode.canonicalScriptId()) === uiSourceCode) {
508
- this.idToUISourceCode.delete(uiSourceCode.canonicalScriptId());
889
+ for (const [k, v] of this.idToUISourceCode) {
890
+ if (v === uiSourceCode) {
891
+ this.idToUISourceCode.delete(k);
892
+ }
509
893
  }
510
- this.removeSourceFrame(uiSourceCode);
511
894
  }
512
- this.tabbedPane.closeTabs(tabIds);
895
+ tabIds.forEach(id => this.tabClosed(id, false));
896
+ this.#scheduleUpdate();
513
897
  }
514
898
 
515
899
  private editorClosedByUserAction(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
@@ -523,7 +907,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
523
907
 
524
908
  private updateHistory(): void {
525
909
  const historyItemKeys = [];
526
- for (const tabId of this.tabbedPane.lastOpenedTabIds(MAX_PREVIOUSLY_VIEWED_FILES_COUNT)) {
910
+ for (const tabId of this.#tabsHistory.slice(0, MAX_PREVIOUSLY_VIEWED_FILES_COUNT)) {
527
911
  const uiSourceCode = this.files.get(tabId);
528
912
  if (uiSourceCode !== undefined) {
529
913
  historyItemKeys.push(historyItemKey(uiSourceCode));
@@ -538,24 +922,27 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
538
922
  return uiSourceCode.url();
539
923
  }
540
924
 
541
- private appendFileTab(
542
- uiSourceCode: Workspace.UISourceCode.UISourceCode, userGesture?: boolean, index?: number,
543
- replaceView?: UI.Widget.Widget): string {
544
- const view = replaceView || this.viewForFile(uiSourceCode);
545
- const title = this.titleForFile(uiSourceCode);
546
- const tooltip = this.tooltipForFile(uiSourceCode);
547
-
925
+ private appendFileTab(uiSourceCode: Workspace.UISourceCode.UISourceCode, index?: number,
926
+ ignoreHistory?: boolean): string {
548
927
  const tabId = this.generateTabId();
549
928
  this.tabIds.set(uiSourceCode, tabId);
550
- this.files.set(tabId, uiSourceCode);
551
929
 
552
- if (!replaceView) {
553
- const savedSelectionRange = this.history.selectionRange(historyItemKey(uiSourceCode));
554
- const savedScrollLineNumber = this.history.scrollLineNumber(historyItemKey(uiSourceCode));
930
+ if (index !== undefined) {
931
+ const entries = Array.from(this.files.entries());
932
+ entries.splice(index, 0, [tabId, uiSourceCode]);
933
+ this.files = new Map(entries);
934
+ } else {
935
+ this.files.set(tabId, uiSourceCode);
936
+ }
937
+
938
+ const savedSelectionRange = this.history.selectionRange(historyItemKey(uiSourceCode));
939
+ const savedScrollLineNumber = this.history.scrollLineNumber(historyItemKey(uiSourceCode));
940
+ const view = this.viewForFile(uiSourceCode);
941
+ if (view && !ignoreHistory) {
555
942
  this.restoreEditorProperties(view, savedSelectionRange, savedScrollLineNumber);
556
943
  }
557
944
 
558
- this.tabbedPane.appendTab(tabId, title, view, tooltip, userGesture, undefined, undefined, index, 'editor');
945
+ this.#scheduleUpdate();
559
946
 
560
947
  this.updateFileTitle(uiSourceCode);
561
948
  this.addUISourceCodeListeners(uiSourceCode);
@@ -568,22 +955,23 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
568
955
  }
569
956
  });
570
957
  }
958
+
959
+ if (!this.#currentFile) {
960
+ this.#showFile(uiSourceCode, false);
961
+ }
571
962
  return tabId;
572
963
  }
573
964
 
574
965
  private addLoadErrorIcon(tabId: string): void {
575
- // clang-format off
576
- const icon = html`<devtools-icon class="small" name="cross-circle-filled"
577
- title=${i18nString(UIStrings.unableToLoadThisContent)}>
578
- </devtools-icon>`;
579
- // clang-format on
580
- if (this.tabbedPane.tabView(tabId)) {
581
- this.tabbedPane.setTrailingTabIcon(tabId, icon);
966
+ const uiSourceCode = this.files.get(tabId);
967
+ if (uiSourceCode) {
968
+ this.#loadErrorFiles.add(uiSourceCode);
969
+ this.#scheduleUpdate();
582
970
  }
583
971
  }
584
972
 
585
- private restoreEditorProperties(
586
- editorView: UI.Widget.Widget, selection?: TextUtils.TextRange.TextRange, firstLineNumber?: number): void {
973
+ private restoreEditorProperties(editorView: UI.Widget.Widget, selection?: TextUtils.TextRange.TextRange,
974
+ firstLineNumber?: number): void {
587
975
  const sourceFrame = editorView instanceof SourceFrame.SourceFrame.SourceFrameImpl ? editorView : null;
588
976
  if (!sourceFrame) {
589
977
  return;
@@ -596,8 +984,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
596
984
  }
597
985
  }
598
986
 
599
- private tabClosed(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void {
600
- const {tabId, isUserGesture} = event.data;
987
+ private tabClosed(tabId: string, isUserGesture?: boolean): void {
601
988
  const uiSourceCode = this.files.get(tabId);
602
989
  if (this.#currentFile && this.#currentFile.canonicalScriptId() === uiSourceCode?.canonicalScriptId()) {
603
990
  this.removeViewListeners();
@@ -606,6 +993,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
606
993
  }
607
994
  if (uiSourceCode) {
608
995
  this.tabIds.delete(uiSourceCode);
996
+ this.#loadErrorFiles.delete(uiSourceCode);
609
997
  }
610
998
  this.files.delete(tabId);
611
999
 
@@ -614,6 +1002,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
614
1002
  this.removeSourceFrame(uiSourceCode);
615
1003
 
616
1004
  this.dispatchEventToListeners(Events.EDITOR_CLOSED, uiSourceCode);
1005
+ this.onEditorClosed?.(uiSourceCode);
617
1006
 
618
1007
  if (isUserGesture) {
619
1008
  this.editorClosedByUserAction(uiSourceCode);
@@ -621,9 +1010,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
621
1010
  }
622
1011
  }
623
1012
 
624
- private tabSelected(event: Common.EventTarget.EventTargetEvent<UI.TabbedPane.EventData>): void {
625
- const {tabId, isUserGesture} = event.data;
626
-
1013
+ private tabSelected(tabId: string, isUserGesture?: boolean): void {
627
1014
  const uiSourceCode = this.files.get(tabId);
628
1015
  if (uiSourceCode) {
629
1016
  this.#showFile(uiSourceCode, isUserGesture);
@@ -632,70 +1019,26 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
632
1019
 
633
1020
  private addUISourceCodeListeners(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
634
1021
  uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
635
- uiSourceCode.addEventListener(
636
- Workspace.UISourceCode.Events.WorkingCopyChanged, this.uiSourceCodeWorkingCopyChanged, this);
637
- uiSourceCode.addEventListener(
638
- Workspace.UISourceCode.Events.WorkingCopyCommitted, this.uiSourceCodeWorkingCopyCommitted, this);
1022
+ uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, this.uiSourceCodeWorkingCopyChanged,
1023
+ this);
1024
+ uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted,
1025
+ this.uiSourceCodeWorkingCopyCommitted, this);
639
1026
  }
640
1027
 
641
1028
  private removeUISourceCodeListeners(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
642
1029
  uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
643
- uiSourceCode.removeEventListener(
644
- Workspace.UISourceCode.Events.WorkingCopyChanged, this.uiSourceCodeWorkingCopyChanged, this);
645
- uiSourceCode.removeEventListener(
646
- Workspace.UISourceCode.Events.WorkingCopyCommitted, this.uiSourceCodeWorkingCopyCommitted, this);
1030
+ uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged,
1031
+ this.uiSourceCodeWorkingCopyChanged, this);
1032
+ uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted,
1033
+ this.uiSourceCodeWorkingCopyCommitted, this);
647
1034
  }
648
1035
 
649
1036
  private updateFileTitle(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
650
- const tabId = this.tabIds.get(uiSourceCode);
651
- if (tabId) {
652
- const title = this.titleForFile(uiSourceCode);
653
- const tooltip = this.tooltipForFile(uiSourceCode);
654
- this.tabbedPane.changeTabTitle(tabId, title, tooltip);
655
- if (uiSourceCode.loadError()) {
656
- // clang-format off
657
- const icon = html`<devtools-icon class="small" name="cross-circle-filled"
658
- title=${i18nString(UIStrings.unableToLoadThisContent)}>
659
- </devtools-icon>`;
660
- // clang-format on
661
- this.tabbedPane.setTrailingTabIcon(tabId, icon);
662
- } else if (Persistence.Persistence.PersistenceImpl.instance().hasUnsavedCommittedChanges(uiSourceCode)) {
663
- /* eslint-disable @devtools/no-imperative-dom-api --
664
- * This is a temporary solution using the <devtools-tooltip>
665
- * and we will use a toast instead once available.
666
- **/
667
- const suffixElement = document.createElement('div');
668
- const icon = new Icon();
669
- icon.name = 'warning-filled';
670
- icon.classList.add('small');
671
- const id = `tab-tooltip-${nextTooltipId++}`;
672
- icon.setAttribute('aria-describedby', id);
673
- const tooltip = new Tooltips.Tooltip.Tooltip({id, anchor: icon, variant: 'rich'});
674
- const automaticFileSystemManager = Persistence.AutomaticFileSystemManager.AutomaticFileSystemManager.instance();
675
- const {automaticFileSystem} = automaticFileSystemManager;
676
- if (automaticFileSystem?.state === 'disconnected') {
677
- const link = document.createElement('a');
678
- link.className = 'devtools-link';
679
- link.textContent = Common.ParsedURL.ParsedURL.extractName(automaticFileSystem.root);
680
- link.addEventListener('click', async event => {
681
- event.consume();
682
- await UI.ViewManager.ViewManager.instance().showView('navigator-files');
683
- await automaticFileSystemManager.connectAutomaticFileSystem(/* addIfMissing= */ true);
684
- });
685
- tooltip.append(uiI18n.getFormatLocalizedString(
686
- str_, UIStrings.changesWereNotSavedToFileSystemToSaveAddFolderToWorkspace, {PH1: link}));
687
- } else {
688
- const link = Link.create('https://developer.chrome.com/docs/devtools/workspaces/', 'Workspace');
689
- tooltip.append(uiI18n.getFormatLocalizedString(
690
- str_, UIStrings.changesWereNotSavedToFileSystemToSaveSetUpYourWorkspace, {PH1: link}));
691
- }
692
- suffixElement.append(icon, tooltip);
693
- /* eslint-enable @devtools/no-imperative-dom-api */
694
- this.tabbedPane.setSuffixElement(tabId, suffixElement);
695
- } else {
696
- const icon = PanelCommon.PersistenceUtils.PersistenceUtils.iconForUISourceCode(uiSourceCode);
697
- this.tabbedPane.setTrailingTabIcon(tabId, icon);
1037
+ if (this.tabIds.has(uiSourceCode)) {
1038
+ if (!uiSourceCode.loadError()) {
1039
+ this.#loadErrorFiles.delete(uiSourceCode);
698
1040
  }
1041
+ this.#scheduleUpdate();
699
1042
  }
700
1043
  }
701
1044
 
@@ -703,7 +1046,7 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
703
1046
  void {
704
1047
  const uiSourceCode = event.data;
705
1048
 
706
- const widget = this.sourceViewByUISourceCode.get(uiSourceCode);
1049
+ const widget = getViewByUISourceCode(uiSourceCode);
707
1050
  if (widget) {
708
1051
  if (this.#sourceViewTypeForWidget(widget) !== this.#sourceViewTypeForUISourceCode(uiSourceCode)) {
709
1052
  // Remove the existing editor tab and create a new one of the correct type.
@@ -749,60 +1092,6 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
749
1092
  return 'tab-' + (tabId++) as Lowercase<string>;
750
1093
  }
751
1094
 
752
- #renderPlaceholder(placeholderElement: HTMLElement): void {
753
- const shortcuts = [
754
- {actionId: 'quick-open.show', description: i18nString(UIStrings.openFile)},
755
- {actionId: 'quick-open.show-command-menu', description: i18nString(UIStrings.runCommand)},
756
- ];
757
- const separator = Host.Platform.isMac() ? '\u2004' : ' + ';
758
- const shortcutElements = shortcuts.map(shortcut => {
759
- const shortcutKeys = UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction(shortcut.actionId);
760
- if (!shortcutKeys?.[0]) {
761
- return {
762
- description: shortcut.description,
763
- onClick: () => {},
764
- keys: [],
765
- };
766
- }
767
- const action = UI.ActionRegistry.ActionRegistry.instance().getAction(shortcut.actionId);
768
- const keys = shortcutKeys[0].descriptors.flatMap(descriptor => descriptor.name.split(separator));
769
- return {
770
- description: shortcut.description,
771
- onClick: () => {
772
- void action.execute();
773
- },
774
- keys,
775
- };
776
- });
777
-
778
- // clang-format off
779
- // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
780
- render(html`
781
- <div class="tabbed-pane-placeholder-row workspace">
782
- <span class="icon-container">
783
- <devtools-icon name="sync" class="sync-icon"></devtools-icon>
784
- </span>
785
- <span>
786
- ${i18nString(UIStrings.workspaceDropInAFolderToSyncSources)}
787
- <button @click=${this.#addFileSystemClicked.bind(this)}>${i18nString(UIStrings.selectFolder)}</button>
788
- </span>
789
- </div>
790
- <div class="shortcuts-list tabbed-pane-placeholder-row" role="list"
791
- aria-label=${i18nString(UIStrings.sourceViewActions)}>
792
- ${shortcutElements.map(shortcut => !shortcut.keys.length
793
- ? html`<div class="shortcut-line" role="listitem"></div>`
794
- : html`<div class="shortcut-line" role="listitem">
795
- <button @click=${shortcut.onClick}>${shortcut.description}</button>
796
- <span class="shortcuts">
797
- ${shortcut.keys.map(key => html`
798
- <span class="keybinds-key"><span>${key}</span></span>
799
- `)}
800
- </span>
801
- </div>`)}
802
- </div>`, placeholderElement);
803
- // clang-format on
804
- }
805
-
806
1095
  async #addFileSystemClicked(): Promise<void> {
807
1096
  const result = await Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager.instance().addFileSystem();
808
1097
  if (!result) {
@@ -813,49 +1102,35 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
813
1102
  }
814
1103
 
815
1104
  getCreatedSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget|undefined {
816
- return this.sourceViewByUISourceCode.get(uiSourceCode);
817
- }
818
-
819
- viewForFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
820
- return this.getOrCreateSourceView(uiSourceCode);
1105
+ return getViewByUISourceCode(uiSourceCode);
821
1106
  }
822
1107
 
823
1108
  private getOrCreateSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
824
- return this.sourceViewByUISourceCode.get(uiSourceCode) || this.createSourceView(uiSourceCode);
825
- }
826
-
827
- private createSourceView(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
828
- let sourceView;
829
- const contentType = uiSourceCode.contentType();
830
-
831
- if (contentType === Common.ResourceType.resourceTypes.Image || uiSourceCode.mimeType().startsWith('image/')) {
832
- sourceView = new SourceFrame.ImageView.ImageView(uiSourceCode.mimeType(), uiSourceCode);
833
- } else if (contentType === Common.ResourceType.resourceTypes.Font || uiSourceCode.mimeType().includes('font')) {
834
- sourceView = new SourceFrame.FontView.FontView(uiSourceCode.mimeType(), uiSourceCode);
835
- } else if (uiSourceCode.name() === HEADER_OVERRIDES_FILENAME) {
836
- sourceView = new Components.HeadersView.HeadersView(uiSourceCode);
837
- } else {
838
- sourceView = new UISourceCodeFrame(uiSourceCode);
839
- this.#historyManager.trackSourceFrameCursorJumps(sourceView);
1109
+ const view = getViewByUISourceCode(uiSourceCode);
1110
+ if (view) {
1111
+ return view;
840
1112
  }
1113
+ return getOrCreateSourceView(uiSourceCode, sourceView => {
1114
+ if (sourceView instanceof UISourceCodeFrame) {
1115
+ this.#historyManager.trackSourceFrameCursorJumps(sourceView);
1116
+ }
1117
+ uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
1118
+ });
1119
+ }
841
1120
 
842
- this.sourceViewByUISourceCode.set(uiSourceCode, sourceView);
843
- uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
844
- return sourceView;
1121
+ viewForFile(uiSourceCode: Workspace.UISourceCode.UISourceCode): UI.Widget.Widget {
1122
+ return this.getOrCreateSourceView(uiSourceCode);
845
1123
  }
846
1124
 
847
1125
  recycleUISourceCodeFrame(sourceFrame: UISourceCodeFrame, uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
848
1126
  sourceFrame.uiSourceCode().removeEventListener(Workspace.UISourceCode.Events.TitleChanged,
849
1127
  this.uiSourceCodeTitleChanged, this);
850
- this.sourceViewByUISourceCode.delete(sourceFrame.uiSourceCode());
851
- sourceFrame.setUISourceCode(uiSourceCode);
852
- this.sourceViewByUISourceCode.set(uiSourceCode, sourceFrame);
1128
+ recycleUISourceCodeFrame(sourceFrame, uiSourceCode);
853
1129
  uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
854
1130
  }
855
1131
 
856
1132
  private removeSourceFrame(uiSourceCode: Workspace.UISourceCode.UISourceCode): void {
857
- const sourceView = this.sourceViewByUISourceCode.get(uiSourceCode);
858
- this.sourceViewByUISourceCode.delete(uiSourceCode);
1133
+ const sourceView = removeSourceViewCache(uiSourceCode);
859
1134
  if (sourceView) {
860
1135
  uiSourceCode.removeEventListener(Workspace.UISourceCode.Events.TitleChanged, this.uiSourceCodeTitleChanged, this);
861
1136
  }
@@ -897,8 +1172,6 @@ export class TabbedEditorContainer extends TabbedEditorContainerBase {
897
1172
  }
898
1173
  }
899
1174
 
900
- let nextTooltipId = 1;
901
-
902
1175
  export const enum Events {
903
1176
  EDITOR_SELECTED = 'EditorSelected',
904
1177
  EDITOR_CLOSED = 'EditorClosed',
@@ -941,9 +1214,8 @@ export class HistoryItem implements HistoryItemKey {
941
1214
  selectionRange: TextUtils.TextRange.TextRange|undefined;
942
1215
  scrollLineNumber: number|undefined;
943
1216
 
944
- constructor(
945
- url: Platform.DevToolsPath.UrlString, resourceType: Common.ResourceType.ResourceType,
946
- selectionRange?: TextUtils.TextRange.TextRange, scrollLineNumber?: number) {
1217
+ constructor(url: Platform.DevToolsPath.UrlString, resourceType: Common.ResourceType.ResourceType,
1218
+ selectionRange?: TextUtils.TextRange.TextRange, scrollLineNumber?: number) {
947
1219
  this.url = url;
948
1220
  this.resourceType = resourceType;
949
1221
  this.selectionRange = selectionRange;