diff --git a/packages/core/src/browser/icons/add-inverse.svg b/packages/core/src/browser/icons/add-inverse.svg new file mode 100644 index 0000000000000..7d3fd77ffd6d3 --- /dev/null +++ b/packages/core/src/browser/icons/add-inverse.svg @@ -0,0 +1 @@ +add \ No newline at end of file diff --git a/packages/core/src/browser/icons/add.svg b/packages/core/src/browser/icons/add.svg new file mode 100644 index 0000000000000..2b679663e8037 --- /dev/null +++ b/packages/core/src/browser/icons/add.svg @@ -0,0 +1 @@ +add \ No newline at end of file diff --git a/packages/debug/src/browser/style/remove-all-inverse.svg b/packages/core/src/browser/icons/remove-all-inverse.svg similarity index 100% rename from packages/debug/src/browser/style/remove-all-inverse.svg rename to packages/core/src/browser/icons/remove-all-inverse.svg diff --git a/packages/debug/src/browser/style/remove-all.svg b/packages/core/src/browser/icons/remove-all.svg similarity index 100% rename from packages/debug/src/browser/style/remove-all.svg rename to packages/core/src/browser/icons/remove-all.svg diff --git a/packages/core/src/browser/shell/tab-bar-toolbar.tsx b/packages/core/src/browser/shell/tab-bar-toolbar.tsx index 006908129c3a1..5d7ef808e86f5 100644 --- a/packages/core/src/browser/shell/tab-bar-toolbar.tsx +++ b/packages/core/src/browser/shell/tab-bar-toolbar.tsx @@ -128,8 +128,9 @@ export class TabBarToolbar extends ReactWidget { if (iconClass) { classNames.push(iconClass); } + const tooltip = item.tooltip || (command && command.label); return
-
{innerText}
+
{innerText}
; } diff --git a/packages/core/src/browser/source-tree/source-tree.ts b/packages/core/src/browser/source-tree/source-tree.ts index 915402fe5fe14..19ecec1f9a25b 100644 --- a/packages/core/src/browser/source-tree/source-tree.ts +++ b/packages/core/src/browser/source-tree/source-tree.ts @@ -48,6 +48,12 @@ export class SourceTree extends TreeImpl { const updated = existing && Object.assign(existing, { element, parent }); if (CompositeTreeElement.hasElements(element)) { if (updated) { + if (!ExpandableTreeNode.is(updated)) { + Object.assign(updated, { expanded: false }); + } + if (!CompositeTreeNode.is(updated)) { + Object.assign(updated, { children: [] }); + } return updated; } return { @@ -65,6 +71,12 @@ export class SourceTree extends TreeImpl { delete updated.children; } if (updated) { + if (ExpandableTreeNode.is(updated)) { + delete updated.expanded; + } + if (CompositeTreeNode.is(updated)) { + delete updated.children; + } return updated; } return { diff --git a/packages/core/src/browser/source-tree/tree-source.ts b/packages/core/src/browser/source-tree/tree-source.ts index cf7b3d1bf7780..9ac5898e5cbda 100644 --- a/packages/core/src/browser/source-tree/tree-source.ts +++ b/packages/core/src/browser/source-tree/tree-source.ts @@ -17,7 +17,7 @@ // tslint:disable:no-any import { ReactNode } from 'react'; -import { injectable } from 'inversify'; +import { injectable, unmanaged } from 'inversify'; import { Emitter, Event } from '../../common/event'; import { MaybePromise } from '../../common/types'; import { Disposable, DisposableCollection } from '../../common/disposable'; @@ -57,7 +57,7 @@ export abstract class TreeSource implements Disposable { readonly id: string | undefined; readonly placeholder: string | undefined; - constructor(options: TreeSourceOptions = {}) { + constructor(@unmanaged() options: TreeSourceOptions = {}) { this.id = options.id; this.placeholder = options.placeholder; } diff --git a/packages/core/src/browser/style/icons.css b/packages/core/src/browser/style/icons.css index c1d033277eea0..3a7733c66b877 100644 --- a/packages/core/src/browser/style/icons.css +++ b/packages/core/src/browser/style/icons.css @@ -37,3 +37,15 @@ height: 16px; background: var(--theia-icon-open-json) no-repeat; } + +.theia-collapse-all-icon { + background: var(--theia-icon-collapse-all) center center no-repeat; +} + +.theia-remove-all-icon { + background: var(--theia-icon-remove-all) center center no-repeat; +} + +.theia-add-icon { + background: var(--theia-icon-add) center center no-repeat; +} diff --git a/packages/core/src/browser/style/variables-bright.useable.css b/packages/core/src/browser/style/variables-bright.useable.css index e57e075f98b31..a70767ef4b0d6 100644 --- a/packages/core/src/browser/style/variables-bright.useable.css +++ b/packages/core/src/browser/style/variables-bright.useable.css @@ -181,6 +181,8 @@ is not optimized for dense, information rich UIs. --theia-icon-whole-word: url(../icons/whole-word.svg); --theia-icon-refresh: url(../icons/Refresh.svg); --theia-icon-collapse-all: url(../icons/CollapseAll.svg); + --theia-icon-remove-all: url(../icons/remove-all.svg); + --theia-icon-add: url(../icons/add.svg); --theia-icon-clear: url(../icons/clear-search-results.svg); --theia-icon-replace: url(../icons/replace.svg); --theia-icon-replace-all: url(../icons/replace-all.svg); diff --git a/packages/core/src/browser/style/variables-dark.useable.css b/packages/core/src/browser/style/variables-dark.useable.css index bac8b7737d157..06e460d50de9b 100644 --- a/packages/core/src/browser/style/variables-dark.useable.css +++ b/packages/core/src/browser/style/variables-dark.useable.css @@ -181,6 +181,8 @@ is not optimized for dense, information rich UIs. --theia-icon-whole-word: url(../icons/whole-word-dark.svg); --theia-icon-refresh: url(../icons/Refresh_inverse.svg); --theia-icon-collapse-all: url(../icons/CollapseAll_inverse.svg); + --theia-icon-remove-all: url(../icons/remove-all-inverse.svg); + --theia-icon-add: url(../icons/add-inverse.svg); --theia-icon-clear: url(../icons/clear-search-results-dark.svg); --theia-icon-replace: url(../icons/replace-inverse.svg); --theia-icon-replace-all: url(../icons/replace-all-inverse.svg); diff --git a/packages/debug/src/browser/console/debug-console-items.tsx b/packages/debug/src/browser/console/debug-console-items.tsx index ee05afbe78285..c836fd98a503a 100644 --- a/packages/debug/src/browser/console/debug-console-items.tsx +++ b/packages/debug/src/browser/console/debug-console-items.tsx @@ -21,18 +21,24 @@ import { SingleTextInputDialog } from '@theia/core/lib/browser'; import { ConsoleItem, CompositeConsoleItem } from '@theia/console/lib/browser/console-session'; import { DebugSession } from '../debug-session'; +export type DebugSessionProvider = () => DebugSession | undefined; + export class ExpressionContainer implements CompositeConsoleItem { private static readonly BASE_CHUNK_SIZE = 100; - protected readonly session: DebugSession | undefined; + protected readonly sessionProvider: DebugSessionProvider; + protected get session(): DebugSession | undefined { + return this.sessionProvider(); + } + protected variablesReference: number; protected namedVariables: number | undefined; protected indexedVariables: number | undefined; protected readonly startOfVariables: number; constructor(options: ExpressionContainer.Options) { - this.session = options.session; + this.sessionProvider = options.session; this.variablesReference = options.variablesReference || 0; this.namedVariables = options.namedVariables; this.indexedVariables = options.indexedVariables; @@ -72,9 +78,10 @@ export class ExpressionContainer implements CompositeConsoleItem { for (let i = 0; i < numberOfChunks; i++) { const start = this.startOfVariables + i * chunkSize; const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - const { session, variablesReference } = this; + const { variablesReference } = this; result.push(new DebugVirtualVariable({ - session, variablesReference, + session: this.sessionProvider, + variablesReference, namedVariables: 0, indexedVariables: count, startOfVariables: start, @@ -98,7 +105,7 @@ export class ExpressionContainer implements CompositeConsoleItem { const names = new Set(); for (const variable of variables) { if (!names.has(variable.name)) { - result.push(new DebugVariable(this.session, variable, this)); + result.push(new DebugVariable(this.sessionProvider, variable, this)); names.add(variable.name); } } @@ -114,7 +121,7 @@ export class ExpressionContainer implements CompositeConsoleItem { } export namespace ExpressionContainer { export interface Options { - session: DebugSession | undefined, + session: DebugSessionProvider, variablesReference?: number namedVariables?: number indexedVariables?: number @@ -128,7 +135,7 @@ export class DebugVariable extends ExpressionContainer { static stringRegex = /^(['"]).*\1$/; constructor( - protected readonly session: DebugSession | undefined, + session: DebugSessionProvider, protected readonly variable: DebugProtocol.Variable, protected readonly parent: ExpressionContainer ) { @@ -262,6 +269,10 @@ export class ExpressionItem extends ExpressionContainer { get value(): string { return this._value; } + protected _type: string | undefined; + get type(): string | undefined { + return this._type; + } protected _available = false; get available(): boolean { @@ -269,12 +280,16 @@ export class ExpressionItem extends ExpressionContainer { } constructor( - protected readonly expression: string, - protected readonly session: DebugSession | undefined + protected _expression: string, + session: DebugSessionProvider ) { super({ session }); } + get expression(): string { + return this._expression; + } + render(): React.ReactNode { const valueClassNames: string[] = []; if (!this._available) { @@ -282,32 +297,42 @@ export class ExpressionItem extends ExpressionContainer { valueClassNames.push('theia-debug-console-unavailable'); } return
-
{this.expression}
+
{this._expression}
{this._value}
; } async evaluate(context: string = 'repl'): Promise { - if (this.session) { + const session = this.session; + if (session) { try { - const { expression } = this; - const body = await this.session.evaluate(expression, context); - if (body) { - this._value = body.result; - this._available = true; - this.variablesReference = body.variablesReference; - this.namedVariables = body.namedVariables; - this.indexedVariables = body.indexedVariables; - this.elements = undefined; - } + const body = await session.evaluate(this._expression, context); + this.setResult(body); } catch (err) { - this._value = err.message; - this._available = false; + this.setResult(undefined, err.message); } } else { - this._value = 'Please start a debug session to evaluate'; + this.setResult(undefined, 'Please start a debug session to evaluate'); + } + } + + 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; + } else { + this._value = error; + this._type = undefined; this._available = false; + this.variablesReference = 0; + this.namedVariables = undefined; + this.indexedVariables = undefined; } + this.elements = undefined; } } @@ -316,7 +341,7 @@ export class DebugScope extends ExpressionContainer { constructor( protected readonly raw: DebugProtocol.Scope, - protected readonly session: DebugSession + session: DebugSessionProvider ) { super({ session, diff --git a/packages/debug/src/browser/console/debug-console-session.ts b/packages/debug/src/browser/console/debug-console-session.ts index d3975f677c0b8..f549ea41988be 100644 --- a/packages/debug/src/browser/console/debug-console-session.ts +++ b/packages/debug/src/browser/console/debug-console-session.ts @@ -118,7 +118,7 @@ export class DebugConsoleSession extends ConsoleSession { } async execute(value: string): Promise { - const expression = new ExpressionItem(value, this.manager.currentSession); + const expression = new ExpressionItem(value, () => this.manager.currentSession); this.items.push(expression); await expression.evaluate(); this.fireDidChange(); @@ -160,7 +160,7 @@ export class DebugConsoleSession extends ConsoleSession { } const severity = category === 'stderr' ? MessageType.Error : event.body.category === 'console' ? MessageType.Warning : MessageType.Info; if (variablesReference) { - const items = await new ExpressionContainer({ session, variablesReference }).getElements(); + const items = await new ExpressionContainer({ session: () => session, variablesReference }).getElements(); this.items.push(...items); } else if (typeof body.output === 'string') { for (const line of body.output.split('\n')) { diff --git a/packages/debug/src/browser/debug-frontend-application-contribution.ts b/packages/debug/src/browser/debug-frontend-application-contribution.ts index e8700528d7a7c..a9cd3e7263027 100644 --- a/packages/debug/src/browser/debug-frontend-application-contribution.ts +++ b/packages/debug/src/browser/debug-frontend-application-contribution.ts @@ -14,7 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { AbstractViewContribution, ApplicationShell, KeybindingRegistry, Widget } from '@theia/core/lib/browser'; +import { AbstractViewContribution, ApplicationShell, KeybindingRegistry, Widget, CompositeTreeNode } from '@theia/core/lib/browser'; import { injectable, inject } from 'inversify'; import { ThemeService } from '@theia/core/lib/browser/theming'; import { MenuModelRegistry, CommandRegistry, MAIN_MENU_BAR, Command, Emitter, Mutable } from '@theia/core/lib/common'; @@ -42,6 +42,9 @@ import { DebugService } from '../common/debug-service'; import { DebugSchemaUpdater } from './debug-schema-updater'; import { DebugPreferences } from './debug-preferences'; import { TabBarToolbarContribution, TabBarToolbarRegistry, TabBarToolbarItem } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { DebugWatchWidget } from './view/debug-watch-widget'; +import { DebugWatchExpression } from './view/debug-watch-expression'; +import { DebugWatchManager } from './debug-watch-manager'; export namespace DebugMenus { export const DEBUG = [...MAIN_MENU_BAR, '6_debug']; @@ -214,6 +217,42 @@ export namespace DebugCommands { category: DEBUG_CATEGORY, label: 'Copy As Expression', }; + export const WATCH_VARIABLE: Command = { + id: 'debug.variable.watch', + category: DEBUG_CATEGORY, + label: 'Add to Watch', + }; + + export const ADD_WATCH_EXPRESSION: Command = { + id: 'debug.watch.addExpression', + category: DEBUG_CATEGORY, + label: 'Add Watch Expression' + }; + export const EDIT_WATCH_EXPRESSION: Command = { + id: 'debug.watch.editExpression', + category: DEBUG_CATEGORY, + label: 'Edit Watch Expression' + }; + export const COPY_WATCH_EXPRESSION_VALUE: Command = { + id: 'debug.watch.copyExpressionValue', + category: DEBUG_CATEGORY, + label: 'Copy Watch Expression Value' + }; + export const REMOVE_WATCH_EXPRESSION: Command = { + id: 'debug.watch.removeExpression', + category: DEBUG_CATEGORY, + label: 'Remove Watch Expression' + }; + export const COLLAPSE_ALL_WATCH_EXPRESSIONS: Command = { + id: 'debug.watch.collapseAllExpressions', + category: DEBUG_CATEGORY, + label: 'Collapse All Watch Expressions' + }; + export const REMOVE_ALL_WATCH_EXPRESSIONS: Command = { + id: 'debug.watch.removeAllExpressions', + category: DEBUG_CATEGORY, + label: 'Remove All Watch Expresssions' + }; } export namespace DebugThreadContextCommands { export const STEP_OVER = { @@ -354,6 +393,9 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi @inject(DebugPreferences) protected readonly preference: DebugPreferences; + @inject(DebugWatchManager) + protected readonly watchManager: DebugWatchManager; + constructor() { super({ widgetId: DebugWidget.ID, @@ -404,11 +446,13 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi this.schemaUpdater.update(); this.configurations.load(); await this.breakpointManager.load(); + await this.watchManager.load(); } onStop(): void { this.configurations.save(); this.breakpointManager.save(); + this.watchManager.save(); } registerMenus(menus: MenuModelRegistry): void { @@ -479,11 +523,23 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi DebugCommands.COPY_CALL_STACK ); - registerMenuActions(DebugVariablesWidget.CONTEXT_MENU, + registerMenuActions(DebugVariablesWidget.EDIT_MENU, DebugCommands.SET_VARIABLE_VALUE, DebugCommands.COPY_VAIRABLE_VALUE, DebugCommands.COPY_VAIRABLE_AS_EXPRESSION ); + registerMenuActions(DebugVariablesWidget.WATCH_MENU, + DebugCommands.WATCH_VARIABLE + ); + + registerMenuActions(DebugWatchWidget.EDIT_MENU, + { ...DebugCommands.EDIT_WATCH_EXPRESSION, label: 'Edit Expression' }, + { ...DebugCommands.COPY_WATCH_EXPRESSION_VALUE, label: 'Copy Value' } + ); + registerMenuActions(DebugWatchWidget.REMOVE_MENU, + { ...DebugCommands.REMOVE_WATCH_EXPRESSION, label: 'Remove Expression' }, + { ...DebugCommands.REMOVE_ALL_WATCH_EXPRESSIONS, label: 'Remove All Expressions' } + ); registerMenuActions(DebugBreakpointsWidget.EDIT_MENU, DebugCommands.EDIT_BREAKPOINT, @@ -751,6 +807,16 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi isEnabled: () => !!this.selectedVariable && this.selectedVariable.supportCopyAsExpression, isVisible: () => !!this.selectedVariable && this.selectedVariable.supportCopyAsExpression }); + registry.registerCommand(DebugCommands.WATCH_VARIABLE, { + execute: () => { + const { selectedVariable, watch } = this; + if (selectedVariable && watch) { + watch.viewModel.addWatchExpression(selectedVariable.name); + } + }, + isEnabled: () => !!this.selectedVariable && !!this.watch, + isVisible: () => !!this.selectedVariable && !!this.watch, + }); registry.registerCommand(DebugEditorContextCommands.ADD_BREAKPOINT, { execute: () => this.editors.toggleBreakpoint(), @@ -814,6 +880,68 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi registry.registerCommand(DebugBreakpointWidgetCommands.CLOSE, { execute: () => this.editors.closeBreakpoint() }); + + registry.registerCommand(DebugCommands.ADD_WATCH_EXPRESSION, { + execute: widget => { + if (widget instanceof Widget) { + if (widget instanceof DebugWatchWidget) { + widget.viewModel.addWatchExpression(); + } + } else if (this.watch) { + this.watch.viewModel.addWatchExpression(); + } + }, + isEnabled: widget => widget instanceof Widget ? widget instanceof DebugWatchWidget : !!this.watch, + isVisible: widget => widget instanceof Widget ? widget instanceof DebugWatchWidget : !!this.watch + }); + registry.registerCommand(DebugCommands.EDIT_WATCH_EXPRESSION, { + execute: () => { + const { watchExpression } = this; + if (watchExpression) { + watchExpression.open(); + } + }, + isEnabled: () => !!this.watchExpression, + isVisible: () => !!this.watchExpression + }); + registry.registerCommand(DebugCommands.COPY_WATCH_EXPRESSION_VALUE, { + execute: () => this.watchExpression && this.watchExpression.copyValue(), + isEnabled: () => !!this.watchExpression && this.watchExpression.supportCopyValue, + isVisible: () => !!this.watchExpression && this.watchExpression.supportCopyValue + }); + registry.registerCommand(DebugCommands.COLLAPSE_ALL_WATCH_EXPRESSIONS, { + execute: widget => { + if (widget instanceof DebugWatchWidget) { + const root = widget.model.root; + widget.model.collapseAll(CompositeTreeNode.is(root) ? root : undefined); + } + }, + isEnabled: widget => widget instanceof DebugWatchWidget, + isVisible: widget => widget instanceof DebugWatchWidget + }); + registry.registerCommand(DebugCommands.REMOVE_WATCH_EXPRESSION, { + execute: () => { + const { watch, watchExpression } = this; + if (watch && watchExpression) { + watch.viewModel.removeWatchExpression(watchExpression); + } + }, + isEnabled: () => !!this.watchExpression, + isVisible: () => !!this.watchExpression + }); + registry.registerCommand(DebugCommands.REMOVE_ALL_WATCH_EXPRESSIONS, { + execute: widget => { + if (widget instanceof Widget) { + if (widget instanceof DebugWatchWidget) { + widget.viewModel.removeWatchExpressions(); + } + } else if (this.watch) { + this.watch.viewModel.removeWatchExpressions(); + } + }, + isEnabled: widget => widget instanceof Widget ? widget instanceof DebugWatchWidget : !!this.watch, + isVisible: widget => widget instanceof Widget ? widget instanceof DebugWatchWidget : !!this.watch + }); } registerKeybindings(keybindings: KeybindingRegistry): void { @@ -902,9 +1030,30 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi toolbar.registerItem({ id: DebugCommands.REMOVE_ALL_BREAKPOINTS.id, command: DebugCommands.REMOVE_ALL_BREAKPOINTS.id, - icon: 'fa breakpoints-remove-all', + icon: 'theia-remove-all-icon', + priority: 1 + }); + + toolbar.registerItem({ + id: DebugCommands.ADD_WATCH_EXPRESSION.id, + command: DebugCommands.ADD_WATCH_EXPRESSION.id, + icon: 'theia-add-icon', + tooltip: 'Add Expression' + }); + toolbar.registerItem({ + id: DebugCommands.COLLAPSE_ALL_WATCH_EXPRESSIONS.id, + command: DebugCommands.COLLAPSE_ALL_WATCH_EXPRESSIONS.id, + icon: 'theia-collapse-all-icon', + tooltip: 'Collapse All', priority: 1 }); + toolbar.registerItem({ + id: DebugCommands.REMOVE_ALL_WATCH_EXPRESSIONS.id, + command: DebugCommands.REMOVE_ALL_WATCH_EXPRESSIONS.id, + icon: 'theia-remove-all-icon', + tooltip: 'Remove All Expressions', + priority: 2 + }); } protected readonly sessionWidgets = new Map(); @@ -1014,4 +1163,13 @@ export class DebugFrontendApplicationContribution extends AbstractViewContributi return variables && variables.selectedElement instanceof DebugVariable && variables.selectedElement || undefined; } + get watch(): DebugWatchWidget | undefined { + const { currentWidget } = this.shell; + return currentWidget instanceof DebugWatchWidget && currentWidget || undefined; + } + get watchExpression(): DebugWatchExpression | undefined { + const { watch } = this; + return watch && watch.selectedElement instanceof DebugWatchExpression && watch.selectedElement || undefined; + } + } diff --git a/packages/debug/src/browser/debug-frontend-module.ts b/packages/debug/src/browser/debug-frontend-module.ts index bc62cefce199c..794426c694180 100644 --- a/packages/debug/src/browser/debug-frontend-module.ts +++ b/packages/debug/src/browser/debug-frontend-module.ts @@ -51,6 +51,7 @@ import { bindLaunchPreferences } from './preferences/launch-preferences'; import { DebugPrefixConfiguration } from './debug-prefix-configuration'; import { CommandContribution } from '@theia/core/lib/common/command'; import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { DebugWatchManager } from './debug-watch-manager'; export default new ContainerModule((bind: interfaces.Bind) => { bind(DebugCallStackItemTypeKey).toDynamicValue(({ container }) => @@ -100,4 +101,6 @@ export default new ContainerModule((bind: interfaces.Bind) => { bindDebugPreferences(bind); bindLaunchPreferences(bind); + + bind(DebugWatchManager).toSelf().inSingletonScope(); }); diff --git a/packages/debug/src/browser/debug-session-connection.ts b/packages/debug/src/browser/debug-session-connection.ts index 001325011d556..8f8c364c58047 100644 --- a/packages/debug/src/browser/debug-session-connection.ts +++ b/packages/debug/src/browser/debug-session-connection.ts @@ -180,7 +180,21 @@ export class DebugSessionConnection implements Disposable { arguments: args }; + const onDispose = this.toDispose.push(Disposable.create(() => { + const pendingRequest = this.pendingRequests.get(request.seq); + if (pendingRequest) { + pendingRequest({ + type: 'response', + request_seq: request.seq, + command: request.command, + seq: 0, + success: false, + message: 'debug session is closed' + }); + } + })); this.pendingRequests.set(request.seq, (response: K) => { + onDispose.dispose(); if (!response.success) { result.reject(response); } else { diff --git a/packages/debug/src/browser/debug-watch-manager.ts b/packages/debug/src/browser/debug-watch-manager.ts new file mode 100644 index 0000000000000..ff11ce244714e --- /dev/null +++ b/packages/debug/src/browser/debug-watch-manager.ts @@ -0,0 +1,93 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject } from 'inversify'; +import { Emitter } from '@theia/core/lib/common/event'; +import { StorageService } from '@theia/core/lib/browser/storage-service'; + +@injectable() +export class DebugWatchManager { + + @inject(StorageService) + protected readonly storage: StorageService; + + protected readonly onDidChangeEmitter = new Emitter(); + readonly onDidChange = this.onDidChangeEmitter.event; + + protected idSequence = 0; + protected readonly _watchExpressions = new Map(); + + get watchExpressions(): IterableIterator<[number, string]> { + return this._watchExpressions.entries(); + } + + addWatchExpression(expression: string): number { + const id = this.idSequence++; + this._watchExpressions.set(id, expression); + this.onDidChangeEmitter.fire(undefined); + return id; + } + + removeWatchExpression(id: number): boolean { + if (!this._watchExpressions.has(id)) { + return false; + } + this._watchExpressions.delete(id); + this.onDidChangeEmitter.fire(undefined); + return true; + } + + removeWatchExpressions(): void { + if (this._watchExpressions.size) { + this.idSequence = 0; + this._watchExpressions.clear(); + this.onDidChangeEmitter.fire(undefined); + } + } + + async load(): Promise { + const data = await this.storage.getData(this.storageKey, { + expressions: [] + }); + this.restoreState(data); + } + + save(): void { + const data = this.storeState(); + this.storage.setData(this.storageKey, data); + } + + protected get storageKey(): string { + return 'debug:watch'; + } + + protected storeState(): DebugWatchData { + return { + expressions: [...this._watchExpressions.values()] + }; + } + + protected restoreState(state: DebugWatchData): void { + for (const expression of state.expressions) { + this.addWatchExpression(expression); + } + } + +} + +export interface DebugWatchData { + readonly expressions: string[]; +} diff --git a/packages/debug/src/browser/editor/debug-hover-source.tsx b/packages/debug/src/browser/editor/debug-hover-source.tsx index eeb69badb61f3..b86ce847cf24c 100644 --- a/packages/debug/src/browser/editor/debug-hover-source.tsx +++ b/packages/debug/src/browser/editor/debug-hover-source.tsx @@ -60,7 +60,7 @@ export class DebugHoverSource extends TreeSource { return undefined; } if (currentSession.capabilities.supportsEvaluateForHovers) { - const item = new ExpressionItem(expression, currentSession); + const item = new ExpressionItem(expression, () => currentSession); await item.evaluate('hover'); return item.available && item || undefined; } diff --git a/packages/debug/src/browser/model/debug-stack-frame.tsx b/packages/debug/src/browser/model/debug-stack-frame.tsx index e98c4e1cb6810..5a9611bda3174 100644 --- a/packages/debug/src/browser/model/debug-stack-frame.tsx +++ b/packages/debug/src/browser/model/debug-stack-frame.tsx @@ -84,12 +84,16 @@ export class DebugStackFrame extends DebugStackFrameData implements TreeElement return this.scopes || (this.scopes = this.doGetScopes()); } protected async doGetScopes(): Promise { + let response; try { - const response = await this.session.sendRequest('scopes', this.toArgs()); - return response.body.scopes.map(raw => new DebugScope(raw, this.session)); + response = await this.session.sendRequest('scopes', this.toArgs()); } catch (e) { + // no-op: ignore debug adapter errors + } + if (!response) { return []; } + return response.body.scopes.map(raw => new DebugScope(raw, () => this.session)); } protected toArgs(arg?: T): { frameId: number } & T { diff --git a/packages/debug/src/browser/style/debug-bright.useable.css b/packages/debug/src/browser/style/debug-bright.useable.css index ad603fbe020f2..5b1f2aa740cdf 100644 --- a/packages/debug/src/browser/style/debug-bright.useable.css +++ b/packages/debug/src/browser/style/debug-bright.useable.css @@ -9,6 +9,5 @@ --theia-debug-step-out: url('step-out.svg'); --theia-debug-configure: url('configure.svg'); --theia-debug-repl: url('repl.svg'); - --breakpoints-remove-all-url: url('remove-all.svg'); --breakpoints-activate-url: url('breakpoints-activate.svg'); } diff --git a/packages/debug/src/browser/style/debug-dark.useable.css b/packages/debug/src/browser/style/debug-dark.useable.css index 056b2b3595023..b33b4a5274d78 100644 --- a/packages/debug/src/browser/style/debug-dark.useable.css +++ b/packages/debug/src/browser/style/debug-dark.useable.css @@ -9,6 +9,5 @@ --theia-debug-step-out: url('step-out-inverse.svg'); --theia-debug-configure: url('configure-inverse.svg'); --theia-debug-repl: url('repl-inverse.svg'); - --breakpoints-remove-all-url: url('remove-all-inverse.svg'); --breakpoints-activate-url: url('breakpoints-activate-inverse.svg'); } diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index b63dd49fbb75a..56d52040e8867 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -307,9 +307,6 @@ background: var(--theia-debug-step-out) center center no-repeat; } -.breakpoints-remove-all { - background: var(--breakpoints-remove-all-url) center center no-repeat; -} .breakpoints-activate { background: var(--breakpoints-activate-url) center center no-repeat; } diff --git a/packages/debug/src/browser/view/debug-session-widget.ts b/packages/debug/src/browser/view/debug-session-widget.ts index a1856db39606d..b7e611c2ede30 100644 --- a/packages/debug/src/browser/view/debug-session-widget.ts +++ b/packages/debug/src/browser/view/debug-session-widget.ts @@ -24,6 +24,7 @@ import { DebugBreakpointsWidget } from './debug-breakpoints-widget'; import { DebugVariablesWidget } from './debug-variables-widget'; import { DebugToolBar } from './debug-toolbar-widget'; import { DebugViewModel, DebugViewOptions } from './debug-view-model'; +import { DebugWatchWidget } from './debug-watch-widget'; export const DebugSessionWidgetFactory = Symbol('DebugSessionWidgetFactory'); export type DebugSessionWidgetFactory = (options: DebugViewOptions) => DebugSessionWidget; @@ -40,6 +41,7 @@ export class DebugSessionWidget extends BaseWidget implements StatefulWidget, Ap child.bind(DebugThreadsWidget).toDynamicValue(({ container }) => DebugThreadsWidget.createWidget(container)); child.bind(DebugStackFramesWidget).toDynamicValue(({ container }) => DebugStackFramesWidget.createWidget(container)); child.bind(DebugVariablesWidget).toDynamicValue(({ container }) => DebugVariablesWidget.createWidget(container)); + child.bind(DebugWatchWidget).toDynamicValue(({ container }) => DebugWatchWidget.createWidget(container)); child.bind(DebugBreakpointsWidget).toDynamicValue(({ container }) => DebugBreakpointsWidget.createWidget(container)); child.bind(DebugSessionWidget).toSelf(); return child; @@ -68,6 +70,9 @@ export class DebugSessionWidget extends BaseWidget implements StatefulWidget, Ap @inject(DebugVariablesWidget) protected readonly variables: DebugVariablesWidget; + @inject(DebugWatchWidget) + protected readonly watch: DebugWatchWidget; + @inject(DebugBreakpointsWidget) protected readonly breakpoints: DebugBreakpointsWidget; @@ -75,16 +80,18 @@ export class DebugSessionWidget extends BaseWidget implements StatefulWidget, Ap protected init(): void { this.id = 'debug:session:' + this.model.id; this.title.label = this.model.label; + this.title.caption = this.model.label; this.title.closable = true; - this.title.iconClass = 'fa debug-tab-icon'; + this.title.iconClass = 'debug-tab-icon'; this.addClass('theia-session-container'); this.viewContainer = this.viewContainerFactory({ - id: 'debug:view-container' + id: 'debug:view-container:' + this.model.id }); this.viewContainer.addWidget(this.threads, { weight: 30 }); this.viewContainer.addWidget(this.stackFrames, { weight: 20 }); this.viewContainer.addWidget(this.variables, { weight: 10 }); + this.viewContainer.addWidget(this.watch, { weight: 10 }); this.viewContainer.addWidget(this.breakpoints, { weight: 10 }); this.toDispose.pushAll([ diff --git a/packages/debug/src/browser/view/debug-variables-widget.ts b/packages/debug/src/browser/view/debug-variables-widget.ts index 11a11cc15bd5a..5ada69010a664 100644 --- a/packages/debug/src/browser/view/debug-variables-widget.ts +++ b/packages/debug/src/browser/view/debug-variables-widget.ts @@ -24,6 +24,8 @@ import { DebugViewModel } from './debug-view-model'; export class DebugVariablesWidget extends SourceTreeWidget { static CONTEXT_MENU: MenuPath = ['debug-variables-context-menu']; + static EDIT_MENU: MenuPath = [...DebugVariablesWidget.CONTEXT_MENU, 'a_edit']; + static WATCH_MENU: MenuPath = [...DebugVariablesWidget.CONTEXT_MENU, 'b_watch']; static createContainer(parent: interfaces.Container): Container { const child = SourceTreeWidget.createContainer(parent, { contextMenuPath: DebugVariablesWidget.CONTEXT_MENU, diff --git a/packages/debug/src/browser/view/debug-view-model.ts b/packages/debug/src/browser/view/debug-view-model.ts index 338fe5d8bae91..8c097dbd228c3 100644 --- a/packages/debug/src/browser/view/debug-view-model.ts +++ b/packages/debug/src/browser/view/debug-view-model.ts @@ -14,6 +14,7 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ +import debounce from 'p-debounce'; import { injectable, inject, postConstruct } from 'inversify'; import { Disposable, DisposableCollection, Event, Emitter } from '@theia/core/lib/common'; import URI from '@theia/core/lib/common/uri'; @@ -22,6 +23,8 @@ import { DebugSessionManager } from '../debug-session-manager'; import { DebugThread } from '../model/debug-thread'; import { DebugStackFrame } from '../model/debug-stack-frame'; import { DebugBreakpoint } from '../model/debug-breakpoint'; +import { DebugWatchExpression } from './debug-watch-expression'; +import { DebugWatchManager } from '../debug-watch-manager'; export const DebugViewOptions = Symbol('DebugViewOptions'); export interface DebugViewOptions { @@ -34,6 +37,7 @@ export class DebugViewModel implements Disposable { protected readonly onDidChangeEmitter = new Emitter(); readonly onDidChange: Event = this.onDidChangeEmitter.event; protected fireDidChange(): void { + this.refreshWatchExpressions(); this.onDidChangeEmitter.fire(undefined); } @@ -43,9 +47,18 @@ export class DebugViewModel implements Disposable { this.onDidChangeBreakpointsEmitter.fire(uri); } + protected readonly _watchExpressions = new Map(); + + protected readonly onDidChangeWatchExpressionsEmitter = new Emitter(); + readonly onDidChangeWatchExpressions = this.onDidChangeWatchExpressionsEmitter.event; + protected fireDidChangeWatchExpressions(): void { + this.onDidChangeWatchExpressionsEmitter.fire(undefined); + } + protected readonly toDispose = new DisposableCollection( this.onDidChangeEmitter, - this.onDidChangeBreakpointsEmitter + this.onDidChangeBreakpointsEmitter, + this.onDidChangeWatchExpressionsEmitter ); @inject(DebugViewOptions) @@ -54,6 +67,9 @@ export class DebugViewModel implements Disposable { @inject(DebugSessionManager) protected readonly manager: DebugSessionManager; + @inject(DebugWatchManager) + protected readonly watch: DebugWatchManager; + protected readonly _sessions = new Set(); get sessions(): IterableIterator { return this._sessions.values(); @@ -109,6 +125,8 @@ export class DebugViewModel implements Disposable { this.fireDidChangeBreakpoints(uri); } })); + this.updateWatchExpressions(); + this.toDispose.push(this.watch.onDidChange(() => this.updateWatchExpressions())); } dispose(): void { @@ -166,4 +184,69 @@ export class DebugViewModel implements Disposable { this.fireDidChange(); } + get watchExpressions(): IterableIterator { + return this._watchExpressions.values(); + } + + async addWatchExpression(expression: string = ''): Promise { + const watchExpression = new DebugWatchExpression({ + id: Number.MAX_SAFE_INTEGER, + expression, + session: () => this.currentSession, + onDidChange: () => { /* no-op */ } + }); + await watchExpression.open(); + if (!watchExpression.expression) { + return undefined; + } + const id = this.watch.addWatchExpression(watchExpression.expression); + return this._watchExpressions.get(id); + } + + removeWatchExpressions(): void { + this.watch.removeWatchExpressions(); + } + + removeWatchExpression(expression: DebugWatchExpression): void { + this.watch.removeWatchExpression(expression.id); + } + + protected updateWatchExpressions(): void { + let added = false; + const toRemove = new Set(this._watchExpressions.keys()); + for (const [id, expression] of this.watch.watchExpressions) { + toRemove.delete(id); + if (!this._watchExpressions.has(id)) { + added = true; + const watchExpression = new DebugWatchExpression({ + id, + expression, + session: () => this.currentSession, + onDidChange: () => this.fireDidChangeWatchExpressions() + }); + this._watchExpressions.set(id, watchExpression); + watchExpression.evaluate(); + } + } + for (const id of toRemove) { + this._watchExpressions.delete(id); + } + if (added || toRemove.size) { + this.fireDidChangeWatchExpressions(); + } + } + + protected refreshWatchExpressionsQueue = Promise.resolve(); + protected refreshWatchExpressions = debounce(() => { + this.refreshWatchExpressionsQueue = this.refreshWatchExpressionsQueue.then(async () => { + try { + for (const watchExpression of this.watchExpressions) { + await watchExpression.evaluate(); + } + } catch (e) { + console.error('Failed to refresh watch expressions: ', e); + } + }); + }, 50); + } diff --git a/packages/debug/src/browser/view/debug-watch-expression.tsx b/packages/debug/src/browser/view/debug-watch-expression.tsx new file mode 100644 index 0000000000000..3e1d8f142a6ba --- /dev/null +++ b/packages/debug/src/browser/view/debug-watch-expression.tsx @@ -0,0 +1,78 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import * as React from 'react'; +import { SingleTextInputDialog } from '@theia/core/lib/browser/dialogs'; +import { ExpressionItem, DebugSessionProvider } from '../console/debug-console-items'; +import { DebugProtocol } from 'vscode-debugprotocol'; + +export class DebugWatchExpression extends ExpressionItem { + + readonly id: number; + + constructor(protected readonly options: { + id: number, + expression: string, + session: DebugSessionProvider, + onDidChange: () => void + }) { + super(options.expression, options.session); + this.id = options.id; + } + + async evaluate(): Promise { + await super.evaluate('watch'); + } + + protected setResult(body?: DebugProtocol.EvaluateResponse['body']): void { + // overriden to ignore error + super.setResult(body); + this.options.onDidChange(); + } + + render(): React.ReactNode { + return
+ {this._expression}: + {this._value} +
; + } + + async open(): Promise { + const input = new SingleTextInputDialog({ + title: 'Edit Watch Expression', + initialValue: this.expression + }); + const newValue = await input.open(); + if (newValue !== undefined) { + this._expression = newValue; + await this.evaluate(); + } + } + + 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; + +} diff --git a/packages/debug/src/browser/view/debug-watch-source.ts b/packages/debug/src/browser/view/debug-watch-source.ts new file mode 100644 index 0000000000000..fd7a67e3ac3bb --- /dev/null +++ b/packages/debug/src/browser/view/debug-watch-source.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct } from 'inversify'; +import { TreeSource } from '@theia/core/lib/browser/source-tree'; +import { DebugViewModel } from './debug-view-model'; +import { DebugWatchExpression } from './debug-watch-expression'; +import debounce = require('p-debounce'); + +@injectable() +export class DebugWatchSource extends TreeSource { + + @inject(DebugViewModel) + protected readonly model: DebugViewModel; + + constructor() { + super({ + placeholder: 'No expressions' + }); + } + + @postConstruct() + protected init(): void { + this.refresh(); + this.toDispose.push(this.model.onDidChangeWatchExpressions(() => this.refresh())); + } + + protected readonly refresh = debounce(() => this.fireDidChange(), 100); + + async getElements(): Promise> { + return this.model.watchExpressions[Symbol.iterator](); + } + +} diff --git a/packages/debug/src/browser/view/debug-watch-widget.ts b/packages/debug/src/browser/view/debug-watch-widget.ts new file mode 100644 index 0000000000000..a36487bb78b0f --- /dev/null +++ b/packages/debug/src/browser/view/debug-watch-widget.ts @@ -0,0 +1,59 @@ +/******************************************************************************** + * Copyright (C) 2019 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 WITH Classpath-exception-2.0 + ********************************************************************************/ + +import { injectable, inject, postConstruct, interfaces, Container } from 'inversify'; +import { MenuPath } from '@theia/core/lib/common'; +import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree'; +import { DebugWatchSource } from './debug-watch-source'; +import { DebugViewModel } from './debug-view-model'; + +@injectable() +export class DebugWatchWidget extends SourceTreeWidget { + + static CONTEXT_MENU: MenuPath = ['debug-watch-context-menu']; + static EDIT_MENU = [...DebugWatchWidget.CONTEXT_MENU, 'a_edit']; + static REMOVE_MENU = [...DebugWatchWidget.CONTEXT_MENU, 'b_remove']; + static createContainer(parent: interfaces.Container): Container { + const child = SourceTreeWidget.createContainer(parent, { + contextMenuPath: DebugWatchWidget.CONTEXT_MENU, + virtualized: false, + scrollIfActive: true + }); + child.bind(DebugWatchSource).toSelf(); + child.unbind(SourceTreeWidget); + child.bind(DebugWatchWidget).toSelf(); + return child; + } + static createWidget(parent: interfaces.Container): DebugWatchWidget { + return DebugWatchWidget.createContainer(parent).get(DebugWatchWidget); + } + + @inject(DebugViewModel) + readonly viewModel: DebugViewModel; + + @inject(DebugWatchSource) + protected readonly variables: DebugWatchSource; + + @postConstruct() + protected init(): void { + super.init(); + this.id = 'debug:watch:' + this.viewModel.id; + this.title.label = 'Watch'; + this.toDispose.push(this.variables); + this.source = this.variables; + } + +} diff --git a/packages/markers/src/browser/problem/problem-contribution.ts b/packages/markers/src/browser/problem/problem-contribution.ts index 55dbda343ecce..abe963a2ca1da 100644 --- a/packages/markers/src/browser/problem/problem-contribution.ts +++ b/packages/markers/src/browser/problem/problem-contribution.ts @@ -40,7 +40,7 @@ export namespace ProblemsCommands { }; export const COLLAPSE_ALL_TOOLBAR: Command = { id: 'problems.collapse.all.toolbar', - iconClass: 'collapse-all' + iconClass: 'theia-collapse-all-icon' }; export const COPY: Command = { id: 'problems.copy' diff --git a/packages/navigator/src/browser/navigator-contribution.ts b/packages/navigator/src/browser/navigator-contribution.ts index 0edfb7cf173fa..0fb9b29df9990 100644 --- a/packages/navigator/src/browser/navigator-contribution.ts +++ b/packages/navigator/src/browser/navigator-contribution.ts @@ -48,7 +48,7 @@ export namespace FileNavigatorCommands { id: 'navigator.collapse.all', category: 'File', label: 'Collapse Folders in Explorer', - iconClass: 'collapse-all' + iconClass: 'theia-collapse-all-icon' }; export const ADD_ROOT_FOLDER: Command = { id: 'navigator.addRootFolder' diff --git a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts index 742c1b784ba69..45a8a7dcb3851 100644 --- a/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts +++ b/packages/search-in-workspace/src/browser/search-in-workspace-frontend-contribution.ts @@ -55,7 +55,7 @@ export namespace SearchInWorkspaceCommands { id: 'search-in-workspace.collapse-all', category: SEARCH_CATEGORY, label: 'Collapse All', - iconClass: 'collapse-all' + iconClass: 'theia-collapse-all-icon' }; export const CLEAR_ALL: Command = { id: 'search-in-workspace.clear-all', diff --git a/packages/search-in-workspace/src/browser/styles/index.css b/packages/search-in-workspace/src/browser/styles/index.css index 0162a42b5feac..13130d6946f43 100644 --- a/packages/search-in-workspace/src/browser/styles/index.css +++ b/packages/search-in-workspace/src/browser/styles/index.css @@ -68,10 +68,6 @@ background: var(--theia-icon-refresh) no-repeat; } -.p-TabBar-toolbar .item .collapse-all { - background: var(--theia-icon-collapse-all) no-repeat; -} - .p-TabBar-toolbar .item .clear-all { background: var(--theia-icon-clear) no-repeat; }