chrome-devtools-frontend
Version:
Chrome DevTools UI
190 lines (165 loc) • 6.69 kB
text/typescript
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Protocol from '../../generated/protocol.js';
import type * as ScopesCodec from '../../third_party/source-map-scopes-codec/source-map-scopes-codec.js';
import * as i18n from '../i18n/i18n.js';
import type {CallFrame, LocationRange, ScopeChainEntry} from './DebuggerModel.js';
import {type GetPropertiesResult, type RemoteObject, RemoteObjectImpl, RemoteObjectProperty} from './RemoteObject.js';
import {contains} from './SourceMapScopesInfo.js';
const UIStrings = {
/**
* @description Title of a section in the debugger showing local JavaScript variables.
*/
local: 'Local',
/**
* @description Text that refers to closure as a programming term
*/
closure: 'Closure',
/**
* @description Noun that represents a section or block of code in the Debugger Model. Shown in the Sources tab, while paused on a breakpoint.
*/
block: 'Block',
/**
* @description Title of a section in the debugger showing JavaScript variables from the global scope.
*/
global: 'Global',
/**
* @description Text in Scope Chain Sidebar Pane of the Sources panel
*/
returnValue: 'Return value',
} as const;
const str_ = i18n.i18n.registerUIStrings('core/sdk/SourceMapScopeChainEntry.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class SourceMapScopeChainEntry implements ScopeChainEntry {
readonly #callFrame: CallFrame;
readonly #scope: ScopesCodec.OriginalScope;
readonly #range?: ScopesCodec.GeneratedRange;
readonly #isInnerMostFunction: boolean;
readonly #returnValue?: RemoteObject;
/**
* @param isInnerMostFunction If `scope` is the innermost 'function' scope. Only used for labeling as we name the
* scope of the paused function 'Local', while other outer 'function' scopes are named 'Closure'.
*/
constructor(
callFrame: CallFrame, scope: ScopesCodec.OriginalScope, range: ScopesCodec.GeneratedRange|undefined,
isInnerMostFunction: boolean, returnValue: RemoteObject|undefined) {
this.#callFrame = callFrame;
this.#scope = scope;
this.#range = range;
this.#isInnerMostFunction = isInnerMostFunction;
this.#returnValue = returnValue;
}
extraProperties(): RemoteObjectProperty[] {
if (this.#returnValue) {
return [new RemoteObjectProperty(
i18nString(UIStrings.returnValue), this.#returnValue, undefined, undefined, undefined, undefined, undefined,
/* synthetic */ true)];
}
return [];
}
callFrame(): CallFrame {
return this.#callFrame;
}
type(): string {
switch (this.#scope.kind) {
case 'global':
return Protocol.Debugger.ScopeType.Global;
case 'function':
return this.#isInnerMostFunction ? Protocol.Debugger.ScopeType.Local : Protocol.Debugger.ScopeType.Closure;
case 'block':
return Protocol.Debugger.ScopeType.Block;
}
return this.#scope.kind ?? '';
}
typeName(): string {
switch (this.#scope.kind) {
case 'global':
return i18nString(UIStrings.global);
case 'function':
return this.#isInnerMostFunction ? i18nString(UIStrings.local) : i18nString(UIStrings.closure);
case 'block':
return i18nString(UIStrings.block);
}
return this.#scope.kind ?? '';
}
name(): string|undefined {
return this.#scope.name;
}
range(): LocationRange|null {
return null;
}
object(): RemoteObject {
return new SourceMapScopeRemoteObject(this.#callFrame, this.#scope, this.#range);
}
description(): string {
return '';
}
icon(): string|undefined {
return undefined;
}
}
class SourceMapScopeRemoteObject extends RemoteObjectImpl {
readonly #callFrame: CallFrame;
readonly #scope: ScopesCodec.OriginalScope;
readonly #range?: ScopesCodec.GeneratedRange;
constructor(callFrame: CallFrame, scope: ScopesCodec.OriginalScope, range: ScopesCodec.GeneratedRange|undefined) {
super(
callFrame.debuggerModel.runtimeModel(), /* objectId */ undefined, 'object', /* sub type */ undefined,
/* value */ null);
this.#callFrame = callFrame;
this.#scope = scope;
this.#range = range;
}
override async doGetProperties(_ownProperties: boolean, accessorPropertiesOnly: boolean, generatePreview: boolean):
Promise<GetPropertiesResult> {
if (accessorPropertiesOnly) {
return {properties: [], internalProperties: []};
}
const properties: RemoteObjectProperty[] = [];
for (const [index, variable] of this.#scope.variables.entries()) {
const expression = this.#findExpression(index);
if (expression === null) {
properties.push(SourceMapScopeRemoteObject.#unavailableProperty(variable));
continue;
}
// TODO(crbug.com/40277685): Once we can evaluate expressions in scopes other than the innermost one,
// we need to find the find the CDP scope that matches `this.#range` and evaluate in that.
const result = await this.#callFrame.evaluate({expression, generatePreview});
if ('error' in result || result.exceptionDetails) {
// TODO(crbug.com/40277685): Make these errors user-visible to aid tooling developers.
// E.g. show the error on hover or expose it in the developer resources panel.
properties.push(SourceMapScopeRemoteObject.#unavailableProperty(variable));
} else {
properties.push(new RemoteObjectProperty(
variable, result.object, /* enumerable */ false, /* writable */ false, /* isOwn */ true,
/* wasThrown */ false));
}
}
return {properties, internalProperties: []};
}
/** @returns null if the variable is unavailable at the current paused location */
#findExpression(index: number): string|null {
if (!this.#range) {
return null;
}
const expressionOrSubRanges = this.#range.values[index];
if (typeof expressionOrSubRanges === 'string') {
return expressionOrSubRanges;
}
if (expressionOrSubRanges === null) {
return null;
}
const pausedPosition = this.#callFrame.location();
for (const range of expressionOrSubRanges) {
if (contains({start: range.from, end: range.to}, pausedPosition.lineNumber, pausedPosition.columnNumber)) {
return range.value ?? null;
}
}
return null;
}
static #unavailableProperty(name: string): RemoteObjectProperty {
return new RemoteObjectProperty(
name, null, /* enumerable */ false, /* writeable */ false, /* isOwn */ true, /* wasThrown */ false);
}
}