diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index e50b51b45e01daf7d3dd9f11b9c29dc1c460b2d3..e68c111e96c3e41dfb12f5d89b14f000b30169b7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -685,6 +685,11 @@ export class DebugSession implements IDebugSession { }) : Promise.resolve(undefined); } + initializeForTest(raw: RawDebugSession): void { + this.raw = raw; + this.registerListeners(); + } + //---- private private registerListeners(): void { diff --git a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts index 56a9de64e264d1a7fd06058cfbb59e4b695f2ab3..22cd4b426d05da9876848c8ba7128dc5b8b7ee69 100644 --- a/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts +++ b/src/vs/workbench/contrib/debug/common/abstractDebugAdapter.ts @@ -20,6 +20,7 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { private messageCallback: ((message: DebugProtocol.ProtocolMessage) => void) | undefined; protected readonly _onError: Emitter; protected readonly _onExit: Emitter; + private queue: DebugProtocol.ProtocolMessage[] = []; constructor() { this.sequence = 1; @@ -110,29 +111,43 @@ export abstract class AbstractDebugAdapter implements IDebugAdapter { this.messageCallback(message); } else { - switch (message.type) { - case 'event': - if (this.eventCallback) { - this.eventCallback(message); - } - break; - case 'request': - if (this.requestCallback) { - this.requestCallback(message); - } - break; - case 'response': - const response = message; - const clb = this.pendingRequests.get(response.request_seq); - if (clb) { - this.pendingRequests.delete(response.request_seq); - clb(response); - } - break; + // Artificially queueing protocol messages guarantees that any microtasks for + // previous message finish before next message is processed. This is essential + // to guarantee ordering when using promises anywhere along the call path. + this.queue.push(message); + if (this.queue.length === 1) { + setTimeout(() => this.processQueue(), 0); } } } + private processQueue(): void { + const message = this.queue!.shift()!; + switch (message.type) { + case 'event': + if (this.eventCallback) { + this.eventCallback(message); + } + break; + case 'request': + if (this.requestCallback) { + this.requestCallback(message); + } + break; + case 'response': + const response = message; + const clb = this.pendingRequests.get(response.request_seq); + if (clb) { + this.pendingRequests.delete(response.request_seq); + clb(response); + } + break; + } + if (this.queue!.length) { + setTimeout(() => this.processQueue(), 0); + } + } + private internalSend(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { message.type = typ; message.seq = this.sequence++; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts index fc9aa21b00a039af1ac632c3c66303f8c75e62cb..0c745ef6ccd772810081a83bb801bf78d1de5136 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts @@ -8,12 +8,14 @@ import { URI as uri } from 'vs/base/common/uri'; import severity from 'vs/base/common/severity'; import { DebugModel, Expression, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import * as sinon from 'sinon'; -import { MockRawSession } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { MockRawSession, MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; -import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; +import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; import { IBreakpointUpdateData, IDebugSessionOptions } from 'vs/workbench/contrib/debug/common/debug'; import { NullOpenerService } from 'vs/platform/opener/common/opener'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { timeout } from 'vs/base/common/async'; function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); @@ -540,4 +542,26 @@ suite('Debug - Model', () => { assert.equal(grandChild.getReplElements().length, 2); assert.equal(child3.getReplElements().length, 1); }); + + test('repl ordering', async () => { + const session = createMockSession(model); + model.addSession(session); + + const adapter = new MockDebugAdapter(); + const raw = new RawDebugSession(adapter, undefined!, undefined!, undefined!, undefined!, undefined!); + session.initializeForTest(raw); + + await session.addReplExpression(undefined, 'before.1'); + assert.equal(session.getReplElements().length, 3); + assert.equal((session.getReplElements()[0]).value, 'before.1'); + assert.equal((session.getReplElements()[1]).value, 'before.1'); + assert.equal((session.getReplElements()[2]).value, '=before.1'); + + await session.addReplExpression(undefined, 'after.2'); + await timeout(0); + assert.equal(session.getReplElements().length, 6); + assert.equal((session.getReplElements()[3]).value, 'after.2'); + assert.equal((session.getReplElements()[4]).value, '=after.2'); + assert.equal((session.getReplElements()[5]).value, 'after.2'); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index 9ce2b7f792eccb96c3da729b00901e7c104acebc..41250367a7d4d0bb4a9ca76e1ac2ed0c7952193a 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -11,6 +11,7 @@ import { ILaunch, IDebugService, State, IDebugSession, IConfigurationManager, IS import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { CompletionItem } from 'vs/editor/common/modes'; import Severity from 'vs/base/common/severity'; +import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; export class MockDebugService implements IDebugService { @@ -463,3 +464,67 @@ export class MockRawSession { public readonly onDidStop: Event = null!; } + +export class MockDebugAdapter extends AbstractDebugAdapter { + private seq = 0; + + startSession(): Promise { + return Promise.resolve(); + } + + stopSession(): Promise { + return Promise.resolve(); + } + + sendMessage(message: DebugProtocol.ProtocolMessage): void { + setTimeout(() => { + if (message.type === 'request') { + const request = message as DebugProtocol.Request; + switch (request.command) { + case 'evaluate': + this.evaluate(request, request.arguments); + return; + } + this.sendResponseBody(request, {}); + return; + } + }, 0); + } + + sendResponseBody(request: DebugProtocol.Request, body: any) { + const response: DebugProtocol.Response = { + seq: ++this.seq, + type: 'response', + request_seq: request.seq, + command: request.command, + success: true, + body + }; + this.acceptMessage(response); + } + + sendEventBody(event: string, body: any) { + const response: DebugProtocol.Event = { + seq: ++this.seq, + type: 'event', + event, + body + }; + this.acceptMessage(response); + } + + evaluate(request: DebugProtocol.Request, args: DebugProtocol.EvaluateArguments) { + if (args.expression.indexOf('before.') === 0) { + this.sendEventBody('output', { output: args.expression }); + } + + this.sendResponseBody(request, { + result: '=' + args.expression, + variablesReference: 0 + }); + + if (args.expression.indexOf('after.') === 0) { + this.sendEventBody('output', { output: args.expression }); + } + } +}