482 lines
17 KiB
TypeScript
482 lines
17 KiB
TypeScript
// *****************************************************************************
|
|
// Copyright (C) 2018 TypeFox and others.
|
|
//
|
|
// This program and the accompanying materials are made available under the
|
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
// http://www.eclipse.org/legal/epl-2.0.
|
|
//
|
|
// This Source Code may also be made available under the following Secondary
|
|
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
// with the GNU Classpath Exception which is available at
|
|
// https://www.gnu.org/software/classpath/license.html.
|
|
//
|
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
// *****************************************************************************
|
|
|
|
import * as React from '@theia/core/shared/react';
|
|
import { DebugProtocol } from '@vscode/debugprotocol/lib/debugProtocol';
|
|
import { codicon, SingleTextInputDialog } from '@theia/core/lib/browser';
|
|
import { ConsoleItem, CompositeConsoleItem } from '@theia/console/lib/browser/console-session';
|
|
import { DebugSession, formatMessage } from '../debug-session';
|
|
import { Severity } from '@theia/core/lib/common/severity';
|
|
import * as monaco from '@theia/monaco-editor-core';
|
|
import { generateUuid, nls } from '@theia/core';
|
|
|
|
export type DebugSessionProvider = () => DebugSession | undefined;
|
|
|
|
export class ExpressionContainer implements CompositeConsoleItem {
|
|
|
|
private static readonly BASE_CHUNK_SIZE = 100;
|
|
|
|
protected readonly sessionProvider: DebugSessionProvider;
|
|
protected get session(): DebugSession | undefined {
|
|
return this.sessionProvider();
|
|
}
|
|
|
|
readonly id: string | number;
|
|
protected variablesReference: number;
|
|
protected namedVariables: number | undefined;
|
|
protected indexedVariables: number | undefined;
|
|
protected presentationHint: DebugProtocol.VariablePresentationHint | undefined;
|
|
protected readonly startOfVariables: number;
|
|
|
|
constructor(options: ExpressionContainer.Options) {
|
|
this.sessionProvider = options.session;
|
|
this.id = options.id ?? generateUuid();
|
|
this.variablesReference = options.variablesReference || 0;
|
|
this.namedVariables = options.namedVariables;
|
|
this.indexedVariables = options.indexedVariables;
|
|
this.startOfVariables = options.startOfVariables || 0;
|
|
this.presentationHint = options.presentationHint;
|
|
if (this.lazy) {
|
|
(this as CompositeConsoleItem).expandByDefault = () => !this.lazy && !this.session?.autoExpandLazyVariables;
|
|
}
|
|
}
|
|
|
|
render(): React.ReactNode {
|
|
return undefined;
|
|
}
|
|
|
|
get reference(): number | undefined {
|
|
return this.variablesReference;
|
|
}
|
|
|
|
get hasElements(): boolean {
|
|
return !!this.variablesReference && !this.lazy;
|
|
}
|
|
|
|
get lazy(): boolean {
|
|
return !!this.presentationHint?.lazy;
|
|
}
|
|
|
|
async resolveLazy(): Promise<void> {
|
|
const { session, variablesReference, lazy } = this;
|
|
if (!session || !variablesReference || !lazy) {
|
|
return;
|
|
}
|
|
const response = await session.sendRequest('variables', { variablesReference });
|
|
const { variables } = response.body;
|
|
if (variables.length !== 1) {
|
|
return;
|
|
}
|
|
this.handleResolvedLazy(variables[0]);
|
|
}
|
|
|
|
protected handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
|
this.variablesReference = resolved.variablesReference;
|
|
this.namedVariables = resolved.namedVariables;
|
|
this.indexedVariables = resolved.indexedVariables;
|
|
this.presentationHint = resolved.presentationHint;
|
|
}
|
|
|
|
protected elements: Promise<ExpressionContainer[]> | undefined;
|
|
async getElements(): Promise<IterableIterator<ExpressionContainer>> {
|
|
if (!this.hasElements || !this.session) {
|
|
return [][Symbol.iterator]();
|
|
}
|
|
if (!this.elements) {
|
|
this.elements = this.doResolve();
|
|
}
|
|
return (await this.elements)[Symbol.iterator]();
|
|
}
|
|
protected async doResolve(): Promise<ExpressionContainer[]> {
|
|
const result: ExpressionContainer[] = [];
|
|
if (this.namedVariables) {
|
|
await this.fetch(result, 'named');
|
|
}
|
|
if (this.indexedVariables) {
|
|
let chunkSize = ExpressionContainer.BASE_CHUNK_SIZE;
|
|
while (this.indexedVariables > chunkSize * ExpressionContainer.BASE_CHUNK_SIZE) {
|
|
chunkSize *= ExpressionContainer.BASE_CHUNK_SIZE;
|
|
}
|
|
if (this.indexedVariables > chunkSize) {
|
|
const numberOfChunks = Math.ceil(this.indexedVariables / chunkSize);
|
|
for (let i = 0; i < numberOfChunks; i++) {
|
|
const start = this.startOfVariables + i * chunkSize;
|
|
const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize);
|
|
const { variablesReference } = this;
|
|
const name = `[${start}..${start + count - 1}]`;
|
|
result.push(new DebugVirtualVariable({
|
|
session: this.sessionProvider,
|
|
id: `${this.id}:${name}`,
|
|
variablesReference,
|
|
namedVariables: 0,
|
|
indexedVariables: count,
|
|
startOfVariables: start,
|
|
name
|
|
}));
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
await this.fetch(result, 'indexed', this.startOfVariables, this.indexedVariables);
|
|
return result;
|
|
}
|
|
|
|
protected fetch(result: ConsoleItem[], filter: 'named'): Promise<void>;
|
|
protected fetch(result: ConsoleItem[], filter: 'indexed', start: number, count?: number): Promise<void>;
|
|
protected async fetch(result: ConsoleItem[], filter: 'indexed' | 'named', start?: number, count?: number): Promise<void> {
|
|
try {
|
|
const { session } = this;
|
|
if (session) {
|
|
const { variablesReference } = this;
|
|
const response = await session.sendRequest('variables', { variablesReference, filter, start, count });
|
|
const { variables } = response.body;
|
|
const names = new Set<string>();
|
|
const debugVariables: DebugVariable[] = [];
|
|
for (const variable of variables) {
|
|
if (!names.has(variable.name)) {
|
|
const v = new DebugVariable(this.sessionProvider, variable, this);
|
|
debugVariables.push(v);
|
|
result.push(v);
|
|
names.add(variable.name);
|
|
}
|
|
}
|
|
if (session.autoExpandLazyVariables) {
|
|
await Promise.all(debugVariables.map(v => v.lazy && v.resolveLazy()));
|
|
}
|
|
}
|
|
} catch (e) {
|
|
result.push({
|
|
severity: Severity.Error,
|
|
visible: !!e.message,
|
|
render: () => e.message
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
export namespace ExpressionContainer {
|
|
export interface Options {
|
|
session: DebugSessionProvider,
|
|
id?: string | number,
|
|
variablesReference?: number
|
|
namedVariables?: number
|
|
indexedVariables?: number
|
|
startOfVariables?: number
|
|
presentationHint?: DebugProtocol.VariablePresentationHint
|
|
}
|
|
}
|
|
|
|
export class DebugVariable extends ExpressionContainer {
|
|
|
|
static booleanRegex = /^true|false$/i;
|
|
static stringRegex = /^(['"]).*\1$/;
|
|
|
|
constructor(
|
|
session: DebugSessionProvider,
|
|
protected readonly variable: DebugProtocol.Variable,
|
|
readonly parent: ExpressionContainer
|
|
) {
|
|
super({
|
|
session,
|
|
id: `${parent.id}:${variable.name}`,
|
|
variablesReference: variable.variablesReference,
|
|
namedVariables: variable.namedVariables,
|
|
indexedVariables: variable.indexedVariables,
|
|
presentationHint: variable.presentationHint
|
|
});
|
|
}
|
|
|
|
get name(): string {
|
|
return this.variable.name;
|
|
}
|
|
get evaluateName(): string | undefined {
|
|
return this.variable.evaluateName;
|
|
}
|
|
protected _type: string | undefined;
|
|
get type(): string | undefined {
|
|
return this._type || this.variable.type;
|
|
}
|
|
protected _value: string | undefined;
|
|
get value(): string {
|
|
return this._value || this.variable.value;
|
|
}
|
|
|
|
get readOnly(): boolean {
|
|
return this.presentationHint?.attributes?.includes('readOnly') || this.lazy;
|
|
}
|
|
|
|
override render(): React.ReactNode {
|
|
const { type, value, name, lazy } = this;
|
|
return <div className={this.variableClassName}>
|
|
<span title={type || name} className='name' ref={this.setNameRef}>{name}{(value || lazy) && ': '}</span>
|
|
{lazy && <span title={nls.localizeByDefault('Click to expand')} className={codicon('eye') + ' lazy-button'} onClick={this.handleLazyButtonClick} />}
|
|
<span title={value} className='value' ref={this.setValueRef}>{value}</span>
|
|
</div>;
|
|
}
|
|
|
|
private readonly handleLazyButtonClick = () => this.resolveLazy();
|
|
|
|
protected get variableClassName(): string {
|
|
const { type, value } = this;
|
|
const classNames = ['theia-debug-console-variable'];
|
|
if (type === 'number' || type === 'boolean' || type === 'string') {
|
|
classNames.push(type);
|
|
} else if (!isNaN(+value)) {
|
|
classNames.push('number');
|
|
} else if (DebugVariable.booleanRegex.test(value)) {
|
|
classNames.push('boolean');
|
|
} else if (DebugVariable.stringRegex.test(value)) {
|
|
classNames.push('string');
|
|
}
|
|
return classNames.join(' ');
|
|
}
|
|
|
|
protected override handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
|
this._value = resolved.value;
|
|
this._type = resolved.type || this._type;
|
|
super.handleResolvedLazy(resolved);
|
|
this.session?.['onDidResolveLazyVariableEmitter'].fire(this);
|
|
}
|
|
|
|
get supportSetVariable(): boolean {
|
|
return !!this.session && !!this.session.capabilities.supportsSetVariable;
|
|
}
|
|
async setValue(value: string): Promise<void> {
|
|
if (!this.session || value === this.value) {
|
|
return;
|
|
}
|
|
const { name, parent } = this;
|
|
const variablesReference = parent['variablesReference'];
|
|
const response = await this.session.sendRequest('setVariable', { variablesReference, name, value });
|
|
this._value = response.body.value;
|
|
this._type = response.body.type;
|
|
this.variablesReference = response.body.variablesReference || 0;
|
|
this.namedVariables = response.body.namedVariables;
|
|
this.indexedVariables = response.body.indexedVariables;
|
|
this.elements = undefined;
|
|
this.session['fireDidChange']();
|
|
}
|
|
|
|
get supportCopyValue(): boolean {
|
|
return !!this.valueRef && document.queryCommandSupported('copy');
|
|
}
|
|
copyValue(): void {
|
|
const selection = document.getSelection();
|
|
if (this.valueRef && selection) {
|
|
selection.selectAllChildren(this.valueRef);
|
|
document.execCommand('copy');
|
|
}
|
|
}
|
|
protected valueRef: HTMLSpanElement | undefined;
|
|
protected setValueRef = (valueRef: HTMLSpanElement | null) => this.valueRef = valueRef || undefined;
|
|
|
|
get supportCopyAsExpression(): boolean {
|
|
return !!this.nameRef && document.queryCommandSupported('copy');
|
|
}
|
|
copyAsExpression(): void {
|
|
const selection = document.getSelection();
|
|
if (this.nameRef && selection) {
|
|
selection.selectAllChildren(this.nameRef);
|
|
document.execCommand('copy');
|
|
}
|
|
}
|
|
protected nameRef: HTMLSpanElement | undefined;
|
|
protected setNameRef = (nameRef: HTMLSpanElement | null) => this.nameRef = nameRef || undefined;
|
|
|
|
async open(): Promise<void> {
|
|
if (!this.supportSetVariable || this.readOnly) {
|
|
return;
|
|
}
|
|
const input = new SingleTextInputDialog({
|
|
title: nls.localize('theia/debug/debugVariableInput', 'Set {0} Value', this.name),
|
|
initialValue: this.value,
|
|
placeholder: nls.localizeByDefault('Value'),
|
|
validate: async (value, mode) => {
|
|
if (!value) {
|
|
return false;
|
|
}
|
|
if (mode === 'open') {
|
|
try {
|
|
await this.setValue(value);
|
|
} catch (error) {
|
|
console.error('setValue failed:', error);
|
|
if (error.body?.error) {
|
|
const errorMessage: DebugProtocol.Message = error.body.error;
|
|
if (errorMessage.showUser) {
|
|
return formatMessage(errorMessage.format, errorMessage.variables);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
});
|
|
await input.open();
|
|
}
|
|
|
|
}
|
|
|
|
export class DebugVirtualVariable extends ExpressionContainer {
|
|
|
|
constructor(
|
|
protected readonly options: VirtualVariableItem.Options
|
|
) {
|
|
super(options);
|
|
}
|
|
|
|
override render(): React.ReactNode {
|
|
return this.options.name;
|
|
}
|
|
}
|
|
export namespace VirtualVariableItem {
|
|
export interface Options extends ExpressionContainer.Options {
|
|
name: string
|
|
}
|
|
}
|
|
|
|
export class ExpressionItem extends ExpressionContainer {
|
|
|
|
severity?: Severity;
|
|
static notAvailable = nls.localizeByDefault('not available');
|
|
|
|
protected _value = ExpressionItem.notAvailable;
|
|
get value(): string {
|
|
return this._value;
|
|
}
|
|
protected _type: string | undefined;
|
|
get type(): string | undefined {
|
|
return this._type;
|
|
}
|
|
|
|
protected _available = false;
|
|
get available(): boolean {
|
|
return this._available;
|
|
}
|
|
|
|
constructor(
|
|
protected _expression: string,
|
|
session: DebugSessionProvider,
|
|
id?: string | number
|
|
) {
|
|
super({ session, id });
|
|
}
|
|
|
|
get expression(): string {
|
|
return this._expression;
|
|
}
|
|
|
|
override render(): React.ReactNode {
|
|
const valueClassNames: string[] = [];
|
|
if (!this._available) {
|
|
valueClassNames.push(ConsoleItem.errorClassName);
|
|
valueClassNames.push('theia-debug-console-unavailable');
|
|
}
|
|
return <div className={'theia-debug-console-expression'}>
|
|
<div>{this._expression}</div>
|
|
<div className={valueClassNames.join(' ')}>{this._value}</div>
|
|
</div>;
|
|
}
|
|
|
|
async evaluate(context: string = 'repl', resolveLazy = true): Promise<void> {
|
|
const session = this.session;
|
|
if (!session?.currentFrame) {
|
|
this.setResult(undefined, ExpressionItem.notAvailable);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const body = await session.evaluate(this._expression, context);
|
|
this.setResult(body);
|
|
if (this.lazy && resolveLazy) {
|
|
await this.resolveLazy();
|
|
}
|
|
} catch (err) {
|
|
this.setResult(undefined, err.message);
|
|
}
|
|
}
|
|
|
|
protected setResult(body?: DebugProtocol.EvaluateResponse['body'], error: string = ExpressionItem.notAvailable): void {
|
|
if (body) {
|
|
this._value = body.result;
|
|
this._type = body.type;
|
|
this._available = true;
|
|
this.variablesReference = body.variablesReference;
|
|
this.namedVariables = body.namedVariables;
|
|
this.indexedVariables = body.indexedVariables;
|
|
this.presentationHint = body.presentationHint;
|
|
this.severity = Severity.Log;
|
|
} else {
|
|
this._value = error;
|
|
this._type = undefined;
|
|
this._available = false;
|
|
this.variablesReference = 0;
|
|
this.namedVariables = undefined;
|
|
this.indexedVariables = undefined;
|
|
this.presentationHint = undefined;
|
|
this.severity = Severity.Error;
|
|
}
|
|
this.elements = undefined;
|
|
}
|
|
|
|
protected override handleResolvedLazy(resolved: DebugProtocol.Variable): void {
|
|
this._value = resolved.value;
|
|
this._type = resolved.type || this._type;
|
|
super.handleResolvedLazy(resolved);
|
|
}
|
|
}
|
|
|
|
export class DebugScope extends ExpressionContainer {
|
|
|
|
constructor(
|
|
protected readonly raw: DebugProtocol.Scope,
|
|
session: DebugSessionProvider,
|
|
id: number
|
|
) {
|
|
super({
|
|
session,
|
|
id: `${raw.name}:${id}`,
|
|
variablesReference: raw.variablesReference,
|
|
namedVariables: raw.namedVariables,
|
|
indexedVariables: raw.indexedVariables
|
|
});
|
|
}
|
|
|
|
override render(): React.ReactNode {
|
|
return this.name;
|
|
}
|
|
|
|
get expensive(): boolean {
|
|
return this.raw.expensive;
|
|
}
|
|
|
|
get range(): monaco.Range | undefined {
|
|
const { line, column, endLine, endColumn } = this.raw;
|
|
if (line !== undefined && column !== undefined && endLine !== undefined && endColumn !== undefined) {
|
|
return new monaco.Range(line, column, endLine, endColumn);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
get name(): string {
|
|
return this.raw.name;
|
|
}
|
|
|
|
expandByDefault(): boolean {
|
|
return this.raw.presentationHint === 'locals';
|
|
}
|
|
|
|
}
|