diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 9cb124d17bd3a15935ad8e2a84037a16de60762b..26ccaab1f0685d18a2af273b0efff6579d648fb3 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -28,7 +28,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -52,6 +51,7 @@ export class DebugSession implements IDebugSession { private rawListeners: IDisposable[] = []; private fetchThreadsScheduler: RunOnceScheduler | undefined; private repl: ReplModel; + private stoppedDetails: IRawStoppedDetails | undefined; private readonly _onDidChangeState = new Emitter(); private readonly _onDidEndAdapter = new Emitter(); @@ -247,7 +247,8 @@ export class DebugSession implements IDebugSession { supportsVariablePaging: true, // #9537 supportsRunInTerminalRequest: true, // #10574 locale: platform.locale, - supportsProgressReporting: true // #92253 + supportsProgressReporting: true, // #92253 + supportsInvalidatedEvent: true // #106745 }); this.initialized = true; @@ -800,6 +801,7 @@ export class DebugSession implements IDebugSession { })); this.rawListeners.push(this.raw.onDidStop(async event => { + this.stoppedDetails = event.body; await this.fetchThreads(event.body); const thread = typeof event.body.threadId === 'number' ? this.getThread(event.body.threadId) : undefined; if (thread) { @@ -1001,6 +1003,19 @@ export class DebugSession implements IDebugSession { this.rawListeners.push(this.raw.onDidProgressEnd(event => { this._onDidProgressEnd.fire(event); })); + this.rawListeners.push(this.raw.onDidInvalidated(async event => { + if (!(event.body.areas && event.body.areas.length === 1 && event.body.areas[0] === 'variables')) { + // If invalidated event only requires to update variables, do that, otherwise refatch threads https://github.com/microsoft/vscode/issues/106745 + this.cancelAllRequests(); + this.model.clearThreads(this.getId(), true); + await this.fetchThreads(this.stoppedDetails); + } + + const viewModel = this.debugService.getViewModel(); + if (viewModel.focusedSession === this) { + viewModel.updateViews(); + } + })); this.rawListeners.push(this.raw.onDidExitAdapter(event => this.onDidExitAdapter(event))); } @@ -1091,7 +1106,7 @@ export class DebugSession implements IDebugSession { async addReplExpression(stackFrame: IStackFrame | undefined, name: string): Promise { await this.repl.addReplExpression(this, stackFrame, name); // Evaluate all watch expressions and fetch variables again since repl evaluation might have changed some. - variableSetEmitter.fire(); + this.debugService.getViewModel().updateViews(); } appendToRepl(data: string | IExpression, severity: severity, source?: IReplElementSource): void { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index dbca94706fed00f34b4fa0245d4f024b1402df28..dce391245dcb664de948b5f7482bc8996b18aef3 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -69,6 +69,7 @@ export class RawDebugSession implements IDisposable { private readonly _onDidProgressStart = new Emitter(); private readonly _onDidProgressUpdate = new Emitter(); private readonly _onDidProgressEnd = new Emitter(); + private readonly _onDidInvalidated = new Emitter(); private readonly _onDidCustomEvent = new Emitter(); private readonly _onDidEvent = new Emitter(); @@ -150,6 +151,9 @@ export class RawDebugSession implements IDisposable { case 'progressEnd': this._onDidProgressEnd.fire(event as DebugProtocol.ProgressEndEvent); break; + case 'invalidated': + this._onDidInvalidated.fire(event as DebugProtocol.InvalidatedEvent); + break; default: this._onDidCustomEvent.fire(event); break; @@ -230,6 +234,10 @@ export class RawDebugSession implements IDisposable { return this._onDidProgressEnd.event; } + get onDidInvalidated(): Event { + return this._onDidInvalidated.event; + } + get onDidEvent(): Event { return this._onDidEvent.event; } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index dd5ccd0d0ad561221250e26bc8139c90ad4593cd..eaad9cd2c6359ee88f71d169430673057a689c10 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -21,7 +21,6 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { Emitter } from 'vs/base/common/event'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; @@ -41,7 +40,6 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; const $ = dom.$; let forgetScopes = true; -export const variableSetEmitter = new Emitter(); let variableInternalContext: Variable | undefined; let dataBreakpointInfoResponse: IDataBreakpointInfoResponse | undefined; @@ -52,7 +50,7 @@ interface IVariablesContext { export class VariablesView extends ViewPane { - private onFocusStackFrameScheduler: RunOnceScheduler; + private updateTreeScheduler: RunOnceScheduler; private needsRefresh = false; private tree!: WorkbenchAsyncDataTree; private savedViewState = new Map(); @@ -85,7 +83,7 @@ export class VariablesView extends ViewPane { this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService); // Use scheduler to prevent unnecessary flashing - this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { + this.updateTreeScheduler = new RunOnceScheduler(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; this.needsRefresh = false; @@ -142,9 +140,9 @@ export class VariablesView extends ViewPane { // Refresh the tree immediately if the user explictly changed stack frames. // Otherwise postpone the refresh until user stops stepping. const timeout = sf.explicit ? 0 : undefined; - this.onFocusStackFrameScheduler.schedule(timeout); + this.updateTreeScheduler.schedule(timeout); })); - this._register(variableSetEmitter.event(() => { + this._register(this.debugService.getViewModel().onWillUpdateViews(() => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; if (stackFrame && forgetScopes) { stackFrame.forgetScopes(); @@ -157,7 +155,7 @@ export class VariablesView extends ViewPane { this._register(this.onDidChangeBodyVisibility(visible => { if (visible && this.needsRefresh) { - this.onFocusStackFrameScheduler.schedule(); + this.updateTreeScheduler.schedule(); } })); let horizontalScrolling: boolean | undefined; @@ -358,7 +356,7 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { .then(() => { // Do not refresh scopes due to a node limitation #15520 forgetScopes = false; - variableSetEmitter.fire(); + this.debugService.getViewModel().updateViews(); }); } } diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 6391f21d7ffb0e24dd4d7444bac2f6883ba7f611..fadd9140817b26895009152d0775645f9eac3a6a 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -25,7 +25,7 @@ import { IDragAndDropData } from 'vs/base/browser/dnd'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { FuzzyScore } from 'vs/base/common/filters'; import { IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; -import { variableSetEmitter, VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { VariablesRenderer } from 'vs/workbench/contrib/debug/browser/variablesView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; @@ -34,12 +34,12 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; -let ignoreVariableSetEmitter = false; +let ignoreViewUpdates = false; let useCachedEvaluation = false; export class WatchExpressionsView extends ViewPane { - private onWatchExpressionsUpdatedScheduler: RunOnceScheduler; + private watchExpressionsUpdatedScheduler: RunOnceScheduler; private needsRefresh = false; private tree!: WorkbenchAsyncDataTree; @@ -58,7 +58,7 @@ export class WatchExpressionsView extends ViewPane { ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { + this.watchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; this.tree.updateChildren(); }, 50); @@ -117,19 +117,19 @@ export class WatchExpressionsView extends ViewPane { return; } - if (!this.onWatchExpressionsUpdatedScheduler.isScheduled()) { - this.onWatchExpressionsUpdatedScheduler.schedule(); + if (!this.watchExpressionsUpdatedScheduler.isScheduled()) { + this.watchExpressionsUpdatedScheduler.schedule(); } })); - this._register(variableSetEmitter.event(() => { - if (!ignoreVariableSetEmitter) { + this._register(this.debugService.getViewModel().onWillUpdateViews(() => { + if (!ignoreViewUpdates) { this.tree.updateChildren(); } })); this._register(this.onDidChangeBodyVisibility(visible => { if (visible && this.needsRefresh) { - this.onWatchExpressionsUpdatedScheduler.schedule(); + this.watchExpressionsUpdatedScheduler.schedule(); } })); let horizontalScrolling: boolean | undefined; @@ -291,9 +291,9 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { onFinish: (value: string, success: boolean) => { if (success && value) { this.debugService.renameWatchExpression(expression.getId(), value); - ignoreVariableSetEmitter = true; - variableSetEmitter.fire(); - ignoreVariableSetEmitter = false; + ignoreViewUpdates = true; + this.debugService.getViewModel().updateViews(); + ignoreViewUpdates = false; } else if (!expression.name) { this.debugService.removeWatchExpressions(expression.getId()); } diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index a2e9b03ecc39a06d0d20150a54ac8313548a5865..8d61d153d220e720d97ea14ae4ada43f9ca232e6 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -437,12 +437,14 @@ export interface IViewModel extends ITreeElement { getSelectedFunctionBreakpoint(): IFunctionBreakpoint | undefined; setSelectedExpression(expression: IExpression | undefined): void; setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint | undefined): void; + updateViews(): void; isMultiSessionView(): boolean; onDidFocusSession: Event; onDidFocusStackFrame: Event<{ stackFrame: IStackFrame | undefined, explicit: boolean }>; onDidSelectExpression: Event; + onWillUpdateViews: Event; } export interface IEvaluate { diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 880028bc8fa8a49454740bf520b1695b807a29da..7a4b1fbaca33298e07468a5fab8c6ff18b6aa22c 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -20,6 +20,7 @@ export class ViewModel implements IViewModel { private readonly _onDidFocusSession = new Emitter(); private readonly _onDidFocusStackFrame = new Emitter<{ stackFrame: IStackFrame | undefined, explicit: boolean }>(); private readonly _onDidSelectExpression = new Emitter(); + private readonly _onWillUpdateViews = new Emitter(); private multiSessionView: boolean; private expressionSelectedContextKey!: IContextKey; private breakpointSelectedContextKey!: IContextKey; @@ -115,6 +116,14 @@ export class ViewModel implements IViewModel { return this.selectedFunctionBreakpoint; } + updateViews(): void { + this._onWillUpdateViews.fire(); + } + + get onWillUpdateViews(): Event { + return this._onWillUpdateViews.event; + } + setSelectedFunctionBreakpoint(functionBreakpoint: IFunctionBreakpoint | undefined): void { this.selectedFunctionBreakpoint = functionBreakpoint; this.breakpointSelectedContextKey.set(!!functionBreakpoint); diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index 39c4e83e985632030fd3ac0510b4aba9c29c3170..ba1d0fa311dcab0bc5e4a38150cecdebf1d359dd 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -10,7 +10,7 @@ import { MockRawSession, createMockDebugModel, mockUriIdentityService } from 'vs import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { Range } from 'vs/editor/common/core/range'; -import { IDebugSessionOptions, State } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugSessionOptions, State, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { NullOpenerService } from 'vs/platform/opener/common/opener'; import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { Constants } from 'vs/base/common/uint'; @@ -19,7 +19,15 @@ import { getStackFrameThreadAndSessionToFocus } from 'vs/workbench/contrib/debug import { generateUuid } from 'vs/base/common/uuid'; export function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); + return new DebugSession(generateUuid(), { resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, { + getViewModel(): any { + return { + updateViews(): void { + // noop + } + }; + } + } as IDebugService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } {