chrome-devtools-frontend 1.0.1703574 → 1.0.1705227

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 (85) hide show
  1. package/AUTHORS +2 -0
  2. package/docs/architecture_of_devtools.md +1 -1
  3. package/docs/cookbook/localization.md +2 -2
  4. package/front_end/core/common/ParsedURL.ts +54 -0
  5. package/front_end/core/common/README.md +4 -3
  6. package/front_end/core/platform/StringUtilities.ts +65 -0
  7. package/front_end/core/sdk/CPUThrottlingManager.ts +3 -3
  8. package/front_end/core/sdk/SourceMap.ts +3 -3
  9. package/front_end/core/sdk/SourceMapScopeChainEntry.ts +4 -3
  10. package/front_end/core/sdk/SourceMapScopesInfo.ts +17 -5
  11. package/front_end/entrypoints/formatter_actions/formatter_actions.ts +18 -0
  12. package/front_end/entrypoints/formatter_worker/Substitute.ts +9 -8
  13. package/front_end/entrypoints/formatter_worker/formatter_worker-entrypoint.ts +2 -2
  14. package/front_end/entrypoints/main/MainImpl.ts +3 -3
  15. package/front_end/foundation/Universe.ts +9 -0
  16. package/front_end/generated/InspectorBackendCommands.ts +3 -2
  17. package/front_end/generated/protocol.ts +16 -0
  18. package/front_end/models/bindings/CompilerScriptMapping.ts +1 -1
  19. package/front_end/models/formatter/FormatterWorkerPool.ts +8 -3
  20. package/front_end/models/har/HARFormat.ts +7 -2
  21. package/front_end/models/source_map_scopes/NamesResolver.ts +75 -38
  22. package/front_end/models/source_map_scopes/ScopeChainModel.ts +13 -45
  23. package/front_end/models/source_map_scopes/ScopeChainResolver.ts +101 -0
  24. package/front_end/models/source_map_scopes/source_map_scopes.ts +2 -0
  25. package/front_end/panels/accessibility/AccessibilityAnnouncementRecordingListView.ts +33 -1
  26. package/front_end/panels/accessibility/AccessibilityAnnouncementRecordingView.ts +4 -12
  27. package/front_end/panels/application/ApplicationPanelSidebar.ts +58 -60
  28. package/front_end/panels/application/BackForwardCacheTreeElement.ts +1 -1
  29. package/front_end/panels/application/FrameDetailsView.ts +64 -82
  30. package/front_end/panels/application/OpenedWindowDetailsView.ts +20 -21
  31. package/front_end/panels/application/PreloadingTreeElement.ts +3 -3
  32. package/front_end/panels/common/CommentsOverlayWidget.ts +145 -72
  33. package/front_end/panels/common/commentsOverlay.css +34 -23
  34. package/front_end/panels/console/ConsolePrompt.ts +2 -1
  35. package/front_end/panels/elements/DOMTreeWidget.ts +29 -10
  36. package/front_end/panels/network/RequestPayloadView.ts +7 -2
  37. package/front_end/panels/network/ShowMoreDetailsWidget.ts +12 -9
  38. package/front_end/panels/sources/DebuggerPlugin.ts +52 -20
  39. package/front_end/panels/sources/ScopeChainSidebarPane.ts +2 -2
  40. package/front_end/panels/sources/SourcesPanel.ts +1 -1
  41. package/front_end/panels/sources/SourcesView.ts +3 -12
  42. package/front_end/panels/sources/TabbedEditorContainer.ts +0 -3
  43. package/front_end/panels/sources/components/HeadersView.ts +193 -123
  44. package/front_end/third_party/chromium/README.chromium +1 -1
  45. package/front_end/third_party/puppeteer/README.chromium +2 -2
  46. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.d.ts +5 -0
  47. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +24 -32
  48. package/front_end/third_party/puppeteer/package/lib/puppeteer/api/ElementHandle.d.ts +1 -1
  49. package/front_end/third_party/puppeteer/package/lib/puppeteer/api/ElementHandle.d.ts.map +1 -1
  50. package/front_end/third_party/puppeteer/package/lib/puppeteer/api/ElementHandle.js +15 -1
  51. package/front_end/third_party/puppeteer/package/lib/puppeteer/api/ElementHandle.js.map +1 -1
  52. package/front_end/third_party/puppeteer/package/lib/puppeteer/cdp/Accessibility.js +8 -1
  53. package/front_end/third_party/puppeteer/package/lib/puppeteer/cdp/Accessibility.js.map +1 -1
  54. package/front_end/third_party/puppeteer/package/lib/puppeteer/common/Debug.d.ts +0 -8
  55. package/front_end/third_party/puppeteer/package/lib/puppeteer/common/Debug.d.ts.map +1 -1
  56. package/front_end/third_party/puppeteer/package/lib/puppeteer/common/Debug.js +0 -24
  57. package/front_end/third_party/puppeteer/package/lib/puppeteer/common/Debug.js.map +1 -1
  58. package/front_end/third_party/puppeteer/package/lib/puppeteer/node/BrowserLauncher.d.ts +12 -0
  59. package/front_end/third_party/puppeteer/package/lib/puppeteer/node/BrowserLauncher.d.ts.map +1 -1
  60. package/front_end/third_party/puppeteer/package/lib/puppeteer/node/BrowserLauncher.js +79 -17
  61. package/front_end/third_party/puppeteer/package/lib/puppeteer/node/BrowserLauncher.js.map +1 -1
  62. package/front_end/third_party/puppeteer/package/lib/puppeteer/node-env-setup.js +1 -1
  63. package/front_end/third_party/puppeteer/package/lib/puppeteer/node-env-setup.js.map +1 -1
  64. package/front_end/third_party/puppeteer/package/lib/puppeteer/revisions.d.ts +3 -3
  65. package/front_end/third_party/puppeteer/package/lib/puppeteer/revisions.js +3 -3
  66. package/front_end/third_party/puppeteer/package/lib/puppeteer/util/version.d.ts +1 -1
  67. package/front_end/third_party/puppeteer/package/lib/puppeteer/util/version.js +1 -1
  68. package/front_end/third_party/puppeteer/package/lib/types.d.ts +5 -0
  69. package/front_end/third_party/puppeteer/package/package.json +4 -4
  70. package/front_end/third_party/puppeteer/package/src/api/ElementHandle.ts +15 -2
  71. package/front_end/third_party/puppeteer/package/src/cdp/Accessibility.ts +10 -1
  72. package/front_end/third_party/puppeteer/package/src/common/Debug.ts +0 -27
  73. package/front_end/third_party/puppeteer/package/src/node/BrowserLauncher.ts +96 -17
  74. package/front_end/third_party/puppeteer/package/src/node-env-setup.ts +1 -1
  75. package/front_end/third_party/puppeteer/package/src/revisions.ts +3 -3
  76. package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
  77. package/front_end/ui/comments/CommentAnchorResolver.ts +60 -22
  78. package/front_end/ui/comments/CommentOverlayManager.ts +78 -25
  79. package/front_end/ui/legacy/README.md +12 -12
  80. package/front_end/ui/legacy/components/data_grid/DataGridExporter.ts +7 -13
  81. package/front_end/ui/legacy/components/inline_editor/PositionAreaEditor.ts +51 -57
  82. package/front_end/ui/legacy/components/source_frame/ImageView.ts +101 -42
  83. package/front_end/ui/legacy/components/utils/Linkifier.ts +1 -2
  84. package/front_end/ui/visual_logging/KnownContextValues.ts +7 -0
  85. package/package.json +1 -1
@@ -9,13 +9,25 @@ import * as Protocol from '../../generated/protocol.js';
9
9
  import type * as Bindings from '../bindings/bindings.js';
10
10
  import * as Formatter from '../formatter/formatter.js';
11
11
 
12
+ /**
13
+ * The result of resolving a generated scope via the source map's mappings/names.
14
+ */
15
+ interface ResolvedScope {
16
+ /** Generated name -> authored name, for generated names that we could map. */
17
+ variableMapping: Map<string, string>;
18
+ thisMapping: string|null;
19
+ /** All generated names declared by the scope, including ones that we couldn't map. */
20
+ generatedNames: string[];
21
+ }
22
+
12
23
  interface CachedScopeMap {
13
24
  sourceMap: SDK.SourceMap.SourceMap|undefined;
14
- mappingPromise: Promise<{variableMapping: Map<string, string>, thisMapping: string|null}>;
25
+ mappingPromise: Promise<ResolvedScope>;
15
26
  }
16
27
 
17
28
  const scopeToCachedIdentifiersMap = new WeakMap<Formatter.FormatterWorkerPool.ScopeTreeNode, CachedScopeMap>();
18
- const cachedMapByCallFrame = new WeakMap<SDK.DebuggerModel.CallFrame, Array<Map<string, string|null>>>();
29
+ const cachedMapByCallFrame =
30
+ new WeakMap<SDK.DebuggerModel.CallFrame, Formatter.FormatterWorkerPool.ScopeVariableMapping[]>();
19
31
 
20
32
  export async function getTextFor(contentProvider: TextUtils.ContentProvider.ContentProvider):
21
33
  Promise<TextUtils.Text.Text|null> {
@@ -119,9 +131,11 @@ export async function findScopeChainForDebuggerScope(scope: SDK.DebuggerModel.Sc
119
131
  const {scopeTree, text} = scopeTreeAndText;
120
132
 
121
133
  // Compute the offset within the scope tree coordinate space.
134
+ const start = script.rawLocationToRelativeLocation(startLocation);
135
+ const end = script.rawLocationToRelativeLocation(endLocation);
122
136
  const scopeOffsets = {
123
- start: text.offsetFromPosition(startLocation.lineNumber, startLocation.columnNumber),
124
- end: text.offsetFromPosition(endLocation.lineNumber, endLocation.columnNumber),
137
+ start: text.offsetFromPosition(start.lineNumber, start.columnNumber),
138
+ end: text.offsetFromPosition(end.lineNumber, end.columnNumber),
125
139
  };
126
140
 
127
141
  return findScopeChain(scopeTree, scopeOffsets);
@@ -191,13 +205,13 @@ const enum Punctuation {
191
205
  const resolveDebuggerScope =
192
206
  async(scope: SDK.DebuggerModel.ScopeChainEntry,
193
207
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
194
- Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
208
+ Promise<ResolvedScope> => {
195
209
  if (!scope.callFrame()
196
210
  .debuggerModel.target()
197
211
  .targetManager()
198
212
  .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
199
213
  .get()) {
200
- return {variableMapping: new Map(), thisMapping: null};
214
+ return {variableMapping: new Map(), thisMapping: null, generatedNames: []};
201
215
  }
202
216
  const script = scope.callFrame().script;
203
217
  const scopeChain = await findScopeChainForDebuggerScope(scope);
@@ -206,10 +220,10 @@ const resolveDebuggerScope =
206
220
 
207
221
  const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.FormatterWorkerPool.ScopeTreeNode[],
208
222
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
209
- Promise<{variableMapping: Map<string, string>, thisMapping: string | null}> => {
223
+ Promise<ResolvedScope> => {
210
224
  const parsedScope = scopeChain[scopeChain.length - 1];
211
225
  if (!parsedScope) {
212
- return {variableMapping: new Map<string, string>(), thisMapping: null};
226
+ return {variableMapping: new Map<string, string>(), thisMapping: null, generatedNames: []};
213
227
  }
214
228
  let cachedScopeMap = scopeToCachedIdentifiersMap.get(parsedScope);
215
229
  const sourceMap = script.sourceMap();
@@ -221,7 +235,7 @@ const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.Form
221
235
  let thisMapping = null;
222
236
 
223
237
  if (!sourceMap) {
224
- return {variableMapping, thisMapping};
238
+ return {variableMapping, thisMapping, generatedNames: []};
225
239
  }
226
240
  // Extract as much as possible from SourceMap and resolve
227
241
  // missing identifier names from SourceMap ranges.
@@ -259,7 +273,7 @@ const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.Form
259
273
 
260
274
  const parsedVariables = await scopeIdentifiers(script, parsedScope, scopeChain.slice(0, -1));
261
275
  if (!parsedVariables) {
262
- return {variableMapping, thisMapping};
276
+ return {variableMapping, thisMapping, generatedNames: []};
263
277
  }
264
278
  for (const id of parsedVariables.boundVariables) {
265
279
  resolveEntry(id, sourceName => {
@@ -277,7 +291,8 @@ const resolveScope = async(script: SDK.Script.Script, scopeChain: Formatter.Form
277
291
  });
278
292
  }
279
293
  await Promise.all(promises).then(getScopeResolvedForTest());
280
- return {variableMapping, thisMapping};
294
+ const generatedNames = parsedVariables.boundVariables.map(id => id.name);
295
+ return {variableMapping, thisMapping, generatedNames};
281
296
  })();
282
297
  cachedScopeMap = {sourceMap, mappingPromise: identifiersPromise};
283
298
  scopeToCachedIdentifiersMap.set(parsedScope, {sourceMap, mappingPromise: identifiersPromise});
@@ -401,23 +416,39 @@ export const resolveScopeChain =
401
416
  return scopes.map(scope => new ScopeWithSourceMappedVariables(scope, thisObject, debuggerWorkspaceBinding));
402
417
  };
403
418
 
404
- function reverseScopeMapping(variableMapping: Map<string, string>): Map<string, string|null> {
405
- const result = new Map<string, string|null>();
406
- for (const [compiledName, originalName] of variableMapping) {
407
- if (originalName && !result.has(originalName)) {
408
- result.set(originalName, compiledName);
419
+ /**
420
+ * Converts the generated name -> authored name mapping of a resolved scope into the
421
+ * authored name -> generated name form expected by `javaScriptSubstitute`.
422
+ */
423
+ function toScopeVariableMapping({variableMapping, generatedNames}: ResolvedScope):
424
+ Formatter.FormatterWorkerPool.ScopeVariableMapping {
425
+ const bindings = new Map<string, string|null>();
426
+ for (const [generatedName, authoredName] of variableMapping) {
427
+ // Multiple generated names can map to the same authored name. We pick the first one. The worker still
428
+ // learns about the others via `generatedNames`, which it needs to detect shadowing of outer bindings.
429
+ if (authoredName && !bindings.has(authoredName)) {
430
+ bindings.set(authoredName, generatedName);
409
431
  }
410
432
  }
411
- return result;
433
+ return {bindings, generatedNames};
412
434
  }
413
435
 
414
436
  /**
415
- * @returns An array of mappings (from inner-most to outer-most scope) of original name -> compiled name or binding expression.
437
+ * Source map scopes don't tell us which generated names a generated range declares. The worker falls back
438
+ * to the free identifiers of the binding expressions in that case.
439
+ */
440
+ function fromSourceMapScopes(mappedVariables: Array<Map<string, string|null>>):
441
+ Formatter.FormatterWorkerPool.ScopeVariableMapping[] {
442
+ return mappedVariables.map(bindings => ({bindings, generatedNames: []}));
443
+ }
444
+
445
+ /**
446
+ * @returns An array of scopes (from inner-most to outer-most) with their original name -> compiled name or binding expression mappings.
416
447
  */
417
448
  export const allVariablesInCallFrame = async(
418
449
  callFrame: SDK.DebuggerModel.CallFrame,
419
450
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
420
- Promise<Array<Map<string, string|null>>> => {
451
+ Promise<Formatter.FormatterWorkerPool.ScopeVariableMapping[]> => {
421
452
  if (!callFrame.debuggerModel.target()
422
453
  .targetManager()
423
454
  .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
@@ -435,36 +466,37 @@ export const allVariablesInCallFrame = async(
435
466
  const mappedVariables =
436
467
  sourceMap?.resolveMappedVariablesAtPosition(callFrame.location(), callFrame.returnValue() !== null);
437
468
  if (mappedVariables) {
438
- cachedMapByCallFrame.set(callFrame, mappedVariables);
439
- return mappedVariables;
469
+ const result = fromSourceMapScopes(mappedVariables);
470
+ cachedMapByCallFrame.set(callFrame, result);
471
+ return result;
440
472
  }
441
473
  }
442
474
 
443
475
  const scopeChain = callFrame.scopeChain().filter(scope => !scope.empty());
444
- const nameMappings =
476
+ const resolvedScopes =
445
477
  await Promise.all(scopeChain.map(scope => resolveDebuggerScope(scope, debuggerWorkspaceBinding)));
446
- const reverseMapping = nameMappings.map(({variableMapping}) => reverseScopeMapping(variableMapping));
447
- cachedMapByCallFrame.set(callFrame, reverseMapping);
448
- return reverseMapping;
478
+ const result = resolvedScopes.map(toScopeVariableMapping);
479
+ cachedMapByCallFrame.set(callFrame, result);
480
+ return result;
449
481
  };
450
482
 
451
483
  /**
452
- * @returns An array of mappings (from inner-most to outer-most scope) of original name -> compiled name or binding expression.
484
+ * @returns An array of scopes (from inner-most to outer-most) with their original name -> compiled name or binding expression mappings.
453
485
  */
454
486
  export const allVariablesAtPosition =
455
487
  async(location: SDK.DebuggerModel.Location,
456
488
  debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
457
- Promise<Array<Map<string, string|null>>> => {
458
- const reverseMapping: Array<Map<string, string|null>> = [];
489
+ Promise<Formatter.FormatterWorkerPool.ScopeVariableMapping[]> => {
490
+ const result: Formatter.FormatterWorkerPool.ScopeVariableMapping[] = [];
459
491
  const script = location.script();
460
492
  if (!script) {
461
- return reverseMapping;
493
+ return result;
462
494
  }
463
495
  if (!script.debuggerModel.target()
464
496
  .targetManager()
465
497
  .settings.resolve(SDK.SDKSettings.jsSourceMapsEnabledSettingDescriptor)
466
498
  .get()) {
467
- return reverseMapping;
499
+ return result;
468
500
  }
469
501
 
470
502
  if (Root.Runtime.hostConfig.devToolsSourceMapScopesInSourcesPanel?.enabled) {
@@ -472,25 +504,25 @@ export const allVariablesAtPosition =
472
504
  script.sourceMap() ?? await script.debuggerModel.sourceMapManager().sourceMapForClientPromise(script);
473
505
  const mappedVariables = sourceMap?.resolveMappedVariablesAtPosition(location);
474
506
  if (mappedVariables) {
475
- return mappedVariables;
507
+ return fromSourceMapScopes(mappedVariables);
476
508
  }
477
509
  }
478
510
 
479
511
  const scopeTreeAndText = await computeScopeTree(script);
480
512
  if (!scopeTreeAndText) {
481
- return reverseMapping;
513
+ return result;
482
514
  }
483
515
 
484
516
  const {scopeTree, text} = scopeTreeAndText;
485
- const locationOffset = text.offsetFromPosition(location.lineNumber, location.columnNumber);
517
+ const {lineNumber, columnNumber} = script.rawLocationToRelativeLocation(location);
518
+ const locationOffset = text.offsetFromPosition(lineNumber, columnNumber);
486
519
  const scopeChain = findScopeChain(scopeTree, {start: locationOffset, end: locationOffset});
487
520
 
488
521
  while (scopeChain.length > 0) {
489
- const {variableMapping} = await resolveScope(script, scopeChain, debuggerWorkspaceBinding);
490
- reverseMapping.push(reverseScopeMapping(variableMapping));
522
+ result.push(toScopeVariableMapping(await resolveScope(script, scopeChain, debuggerWorkspaceBinding)));
491
523
  scopeChain.pop();
492
524
  }
493
- return reverseMapping;
525
+ return result;
494
526
  };
495
527
 
496
528
  export const resolveThisObject = async(
@@ -746,8 +778,8 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
746
778
  * start or if the function scope does not start with a left paren (e.g., arrow
747
779
  * function with one parameter), the resolution returns null.
748
780
  **/
749
- async function getFunctionNameFromScopeStart(
750
- script: SDK.Script.Script, lineNumber: number, columnNumber: number): Promise<string|null> {
781
+ async function getFunctionNameFromScopeStart(script: SDK.Script.Script, rawLineNumber: number,
782
+ rawColumnNumber: number): Promise<string|null> {
751
783
  // To reduce the overhead of resolving function names,
752
784
  // we check for source maps first and immediately leave
753
785
  // this function if the script doesn't have a sourcemap.
@@ -756,6 +788,11 @@ async function getFunctionNameFromScopeStart(
756
788
  return null;
757
789
  }
758
790
 
791
+ // The source map and the script text are relative to the start of the script, whereas
792
+ // raw V8 positions in inline scripts are relative to the start of the document.
793
+ const {lineNumber, columnNumber} =
794
+ script.rawLocationToRelativeLocation({lineNumber: rawLineNumber, columnNumber: rawColumnNumber});
795
+
759
796
  const scopeName = sourceMap.findOriginalFunctionName({line: lineNumber, column: columnNumber});
760
797
  if (scopeName !== null) {
761
798
  return scopeName;
@@ -3,10 +3,9 @@
3
3
  // found in the LICENSE file.
4
4
 
5
5
  import * as Common from '../../core/common/common.js';
6
- import * as SDK from '../../core/sdk/sdk.js';
7
- import type * as Bindings from '../bindings/bindings.js';
6
+ import type * as SDK from '../../core/sdk/sdk.js';
8
7
 
9
- import {resolveScopeChain} from './NamesResolver.js';
8
+ import {Events as ScopeChainResolverEvents, type ScopeChainResolver} from './ScopeChainResolver.js';
10
9
 
11
10
  /**
12
11
  * This class is responsible for resolving / updating the scope chain for a specific {@link SDK.DebuggerModel.CallFrame}
@@ -18,57 +17,35 @@ import {resolveScopeChain} from './NamesResolver.js';
18
17
  *
19
18
  * Source maps can be enabled/disabled dynamically and debugger plugins can attach debug info after the fact.
20
19
  *
21
- * This class tracks all that and sends events with the latest scope chain for a specific call frame.
20
+ * The {@link ScopeChainResolver} tracks all that and invalidates its cache accordingly. This class
21
+ * sends events with the latest scope chain for a specific call frame.
22
22
  */
23
23
  export class ScopeChainModel extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
24
- static readonly #cachedScopeChainByCallFrame =
25
- new WeakMap<SDK.DebuggerModel.CallFrame, Promise<SDK.DebuggerModel.ScopeChainEntry[]>>();
26
-
27
24
  readonly #callFrame: SDK.DebuggerModel.CallFrame;
28
- readonly #debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding;
25
+ readonly #scopeChainResolver: ScopeChainResolver;
29
26
 
30
27
  /** We use the `Throttler` here to make sure that `#boundUpdate` is not run multiple times simultanously */
31
28
  readonly #throttler = new Common.Throttler.Throttler(5);
32
29
  readonly #boundUpdate = this.#update.bind(this);
33
30
 
34
- constructor(callFrame: SDK.DebuggerModel.CallFrame,
35
- debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding) {
31
+ constructor(callFrame: SDK.DebuggerModel.CallFrame, scopeChainResolver: ScopeChainResolver) {
36
32
  super();
37
33
  this.#callFrame = callFrame;
38
- this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
39
- this.#callFrame.debuggerModel.addEventListener(
40
- SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
41
- this.#callFrame.debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
42
- this.#sourceMapChanged, this);
43
- this.#callFrame.debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
44
- this.#sourceMapChanged, this);
34
+ this.#scopeChainResolver = scopeChainResolver;
35
+ this.#scopeChainResolver.addEventListener(ScopeChainResolverEvents.SCOPE_CHAIN_INVALIDATED,
36
+ this.#scopeChainInvalidated, this);
45
37
 
46
38
  void this.#throttler.schedule(this.#boundUpdate);
47
39
  }
48
40
 
49
41
  dispose(): void {
50
- this.#callFrame.debuggerModel.removeEventListener(
51
- SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
52
- this.#callFrame.debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
53
- this.#sourceMapChanged, this);
54
- this.#callFrame.debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
55
- this.#sourceMapChanged, this);
42
+ this.#scopeChainResolver.removeEventListener(ScopeChainResolverEvents.SCOPE_CHAIN_INVALIDATED,
43
+ this.#scopeChainInvalidated, this);
56
44
  this.listeners?.clear();
57
45
  }
58
46
 
59
- static resolveScopeChain(callFrame: SDK.DebuggerModel.CallFrame,
60
- debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding):
61
- Promise<SDK.DebuggerModel.ScopeChainEntry[]> {
62
- let cachedPromise = ScopeChainModel.#cachedScopeChainByCallFrame.get(callFrame);
63
- if (!cachedPromise) {
64
- cachedPromise = resolveScopeChain(callFrame, debuggerWorkspaceBinding);
65
- ScopeChainModel.#cachedScopeChainByCallFrame.set(callFrame, cachedPromise);
66
- }
67
- return cachedPromise;
68
- }
69
-
70
47
  resolveScopeChain(): Promise<SDK.DebuggerModel.ScopeChainEntry[]> {
71
- return ScopeChainModel.resolveScopeChain(this.#callFrame, this.#debuggerWorkspaceBinding);
48
+ return this.#scopeChainResolver.resolveScopeChain(this.#callFrame);
72
49
  }
73
50
 
74
51
  async #update(): Promise<void> {
@@ -76,17 +53,8 @@ export class ScopeChainModel extends Common.ObjectWrapper.ObjectWrapper<EventTyp
76
53
  this.dispatchEventToListeners(Events.SCOPE_CHAIN_UPDATED, new ScopeChain(scopeChain));
77
54
  }
78
55
 
79
- #debugInfoAttached(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
56
+ #scopeChainInvalidated(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
80
57
  if (event.data === this.#callFrame.script) {
81
- ScopeChainModel.#cachedScopeChainByCallFrame.delete(this.#callFrame);
82
- void this.#throttler.schedule(this.#boundUpdate);
83
- }
84
- }
85
-
86
- #sourceMapChanged(event: Common.EventTarget
87
- .EventTargetEvent<{client: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap}>): void {
88
- if (event.data.client === this.#callFrame.script) {
89
- ScopeChainModel.#cachedScopeChainByCallFrame.delete(this.#callFrame);
90
58
  void this.#throttler.schedule(this.#boundUpdate);
91
59
  }
92
60
  }
@@ -0,0 +1,101 @@
1
+ // Copyright 2026 The Chromium Authors
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 * as Common from '../../core/common/common.js';
6
+ import * as Root from '../../core/root/root.js';
7
+ import * as SDK from '../../core/sdk/sdk.js';
8
+ import type * as Bindings from '../bindings/bindings.js';
9
+
10
+ import {resolveScopeChain} from './NamesResolver.js';
11
+
12
+ /**
13
+ * Resolves and caches the scope chain for {@link SDK.DebuggerModel.CallFrame}s.
14
+ *
15
+ * There is one instance per `Universe`. Caching the resolved scope chain allows different consumers
16
+ * (e.g. the Scope view and the inline values in the editor) to share the exact same
17
+ * {@link SDK.DebuggerModel.ScopeChainEntry} and {@link SDK.RemoteObject.RemoteObject} instances for a
18
+ * given pause, without redundant CDP evaluations.
19
+ *
20
+ * The cache for a script is invalidated whenever debugging info (e.g. from a debugger language plugin)
21
+ * or a source map is attached to or detached from that script. The {@link Events.SCOPE_CHAIN_INVALIDATED}
22
+ * event is dispatched afterwards, so consumers can re-resolve the scope chain.
23
+ */
24
+ export class ScopeChainResolver extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
25
+ SDK.TargetManager.SDKModelObserver<SDK.DebuggerModel.DebuggerModel> {
26
+ readonly #debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding;
27
+ readonly #cache = new WeakMap<SDK.Script.Script,
28
+ WeakMap<SDK.DebuggerModel.CallFrame, Promise<SDK.DebuggerModel.ScopeChainEntry[]>>>();
29
+
30
+ constructor(targetManager: SDK.TargetManager.TargetManager,
31
+ debuggerWorkspaceBinding: Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding) {
32
+ super();
33
+ this.#debuggerWorkspaceBinding = debuggerWorkspaceBinding;
34
+ targetManager.observeModels(SDK.DebuggerModel.DebuggerModel, this);
35
+ }
36
+
37
+ /**
38
+ * @deprecated Pass the `ScopeChainResolver` of the `Universe` via constructor instead.
39
+ */
40
+ static instance(): ScopeChainResolver {
41
+ // TODO(crbug.com/458180550): Remove once all callers receive the instance via constructor.
42
+ return Root.DevToolsContext.globalInstance().get(ScopeChainResolver);
43
+ }
44
+
45
+ modelAdded(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
46
+ debuggerModel.addEventListener(SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
47
+ debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
48
+ this.#sourceMapChanged, this);
49
+ debuggerModel.sourceMapManager().addEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
50
+ this.#sourceMapChanged, this);
51
+ }
52
+
53
+ modelRemoved(debuggerModel: SDK.DebuggerModel.DebuggerModel): void {
54
+ debuggerModel.removeEventListener(SDK.DebuggerModel.Events.DebugInfoAttached, this.#debugInfoAttached, this);
55
+ debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapAttached,
56
+ this.#sourceMapChanged, this);
57
+ debuggerModel.sourceMapManager().removeEventListener(SDK.SourceMapManager.Events.SourceMapDetached,
58
+ this.#sourceMapChanged, this);
59
+ }
60
+
61
+ /**
62
+ * Returns the (cached) resolved scope chain for `callFrame`. Repeated calls return the same promise
63
+ * until the cache for the call frame's script is invalidated.
64
+ */
65
+ resolveScopeChain(callFrame: SDK.DebuggerModel.CallFrame): Promise<SDK.DebuggerModel.ScopeChainEntry[]> {
66
+ let cacheForScript = this.#cache.get(callFrame.script);
67
+ if (!cacheForScript) {
68
+ cacheForScript = new WeakMap();
69
+ this.#cache.set(callFrame.script, cacheForScript);
70
+ }
71
+ let cachedPromise = cacheForScript.get(callFrame);
72
+ if (!cachedPromise) {
73
+ cachedPromise = resolveScopeChain(callFrame, this.#debuggerWorkspaceBinding);
74
+ cacheForScript.set(callFrame, cachedPromise);
75
+ }
76
+ return cachedPromise;
77
+ }
78
+
79
+ #invalidate(script: SDK.Script.Script): void {
80
+ this.#cache.delete(script);
81
+ this.dispatchEventToListeners(Events.SCOPE_CHAIN_INVALIDATED, script);
82
+ }
83
+
84
+ #debugInfoAttached(event: Common.EventTarget.EventTargetEvent<SDK.Script.Script>): void {
85
+ this.#invalidate(event.data);
86
+ }
87
+
88
+ #sourceMapChanged(event: Common.EventTarget
89
+ .EventTargetEvent<{client: SDK.Script.Script, sourceMap: SDK.SourceMap.SourceMap}>): void {
90
+ this.#invalidate(event.data.client);
91
+ }
92
+ }
93
+
94
+ export const enum Events {
95
+ SCOPE_CHAIN_INVALIDATED = 'ScopeChainInvalidated',
96
+ }
97
+
98
+ export interface EventTypes {
99
+ /** Dispatched with the script for which the cached scope chains have been invalidated. */
100
+ [Events.SCOPE_CHAIN_INVALIDATED]: SDK.Script.Script;
101
+ }
@@ -5,9 +5,11 @@
5
5
  import * as FunctionCodeResolver from './FunctionCodeResolver.js';
6
6
  import * as NamesResolver from './NamesResolver.js';
7
7
  import * as ScopeChainModel from './ScopeChainModel.js';
8
+ import * as ScopeChainResolver from './ScopeChainResolver.js';
8
9
 
9
10
  export {
10
11
  FunctionCodeResolver,
11
12
  NamesResolver,
12
13
  ScopeChainModel,
14
+ ScopeChainResolver,
13
15
  };
@@ -4,6 +4,7 @@
4
4
 
5
5
  import '../../ui/legacy/components/data_grid/data_grid.js';
6
6
 
7
+ import * as Host from '../../core/host/host.js';
7
8
  import * as i18n from '../../core/i18n/i18n.js';
8
9
  import * as UI from '../../ui/legacy/legacy.js';
9
10
  import * as Lit from '../../ui/lit/lit.js';
@@ -45,6 +46,14 @@ const UIStrings = {
45
46
  * @description Accessible title for the announcements data grid.
46
47
  */
47
48
  ariaLiveRecordingList: 'Accessibility Announcements',
49
+ /**
50
+ * @description Context menu item for copying the announcement message text to the clipboard.
51
+ */
52
+ copyMessage: 'Copy message',
53
+ /**
54
+ * @description Context menu item for copying the announcement element HTML snippet to the clipboard.
55
+ */
56
+ copyElementHtml: 'Copy element HTML',
48
57
  } as const;
49
58
  const str_ =
50
59
  i18n.i18n.registerUIStrings('panels/accessibility/AccessibilityAnnouncementRecordingListView.ts', UIStrings);
@@ -53,6 +62,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
53
62
  export interface ViewInput {
54
63
  items: readonly A11yAnnouncement[];
55
64
  selectedItem: A11yAnnouncement|null;
65
+ onContextMenu: (menu: UI.ContextMenu.ContextMenu, item: A11yAnnouncement) => void;
56
66
  onSelect: (item: A11yAnnouncement) => void;
57
67
  onDeselect: () => void;
58
68
  }
@@ -92,7 +102,12 @@ export const DEFAULT_VIEW: View = (input, _output, target) => {
92
102
  return html`
93
103
  <tr
94
104
  ?selected=${item === input.selectedItem}
95
- @select=${() => input.onSelect(item)}>
105
+ @select=${() => input.onSelect(item)}
106
+ @contextmenu=${(e: CustomEvent<UI.ContextMenu.ContextMenu>) => {
107
+ if (e.detail instanceof UI.ContextMenu.ContextMenu) {
108
+ input.onContextMenu(e.detail, item);
109
+ }
110
+ }}>
96
111
  <td data-value=${item.time}>
97
112
  <span>${timeString}</span>
98
113
  </td>
@@ -159,10 +174,27 @@ export class AccessibilityAnnouncementRecordingListView extends UI.Widget.VBox {
159
174
  this.requestUpdate();
160
175
  }
161
176
 
177
+ #populateContextMenu(contextMenu: UI.ContextMenu.ContextMenu, item: A11yAnnouncement): void {
178
+ if (item.message) {
179
+ contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyMessage), () => {
180
+ Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.message);
181
+ }, {jslogContext: 'copy-message'});
182
+ }
183
+
184
+ if (item.element) {
185
+ contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyElementHtml), () => {
186
+ Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.element);
187
+ }, {jslogContext: 'copy-element-html'});
188
+ }
189
+ }
190
+
162
191
  override performUpdate(): void {
163
192
  const input: ViewInput = {
164
193
  items: this.#items,
165
194
  selectedItem: this.#selectedItem,
195
+ onContextMenu: (contextMenu: UI.ContextMenu.ContextMenu, item: A11yAnnouncement) => {
196
+ this.#populateContextMenu(contextMenu, item);
197
+ },
166
198
  onSelect: (item: A11yAnnouncement) => {
167
199
  this.selectedItem = item;
168
200
  this.#onSelect?.(item);
@@ -744,14 +744,6 @@ export function validateAndSanitizeAnnouncement(payload: unknown): A11yAnnouncem
744
744
  };
745
745
  }
746
746
 
747
- export function escapeCsvValue(val: string): string {
748
- const escaped = val.replace(/"/g, '""');
749
- if (escaped.includes(',') || escaped.includes('\n') || escaped.includes('\r') || escaped.includes('"')) {
750
- return `"${escaped}"`;
751
- }
752
- return escaped;
753
- }
754
-
755
747
  export function buildCsvContent(announcements: readonly A11yAnnouncement[]): string {
756
748
  const csvRows: string[] = [];
757
749
  csvRows.push(['Time', 'API', 'Politeness', 'Message'].join(','));
@@ -759,10 +751,10 @@ export function buildCsvContent(announcements: readonly A11yAnnouncement[]): str
759
751
  for (const item of announcements) {
760
752
  const timeString = new Date(item.time).toISOString();
761
753
  const row = [
762
- escapeCsvValue(timeString),
763
- escapeCsvValue(item.api),
764
- escapeCsvValue(item.politeness),
765
- escapeCsvValue(item.message),
754
+ Platform.StringUtilities.escapeCsvCell(timeString),
755
+ Platform.StringUtilities.escapeCsvCell(item.api),
756
+ Platform.StringUtilities.escapeCsvCell(item.politeness),
757
+ Platform.StringUtilities.escapeCsvCell(item.message),
766
758
  ];
767
759
  csvRows.push(row.join(','));
768
760
  }