// ***************************************************************************** // 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 { 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 | undefined; async getElements(): Promise> { 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 { 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; protected fetch(result: ConsoleItem[], filter: 'indexed', start: number, count?: number): Promise; protected async fetch(result: ConsoleItem[], filter: 'indexed' | 'named', start?: number, count?: number): Promise { 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(); 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
{name}{(value || lazy) && ': '} {lazy && } {value}
; } 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 { 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 { 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
{this._expression}
{this._value}
; } async evaluate(context: string = 'repl', resolveLazy = true): Promise { 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'; } }