diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 00ed0d36faf4c..5b9c4ac37dd35 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -832,6 +832,17 @@ export class DebugSession implements IDebugSession { column: event.body.column ? event.body.column : 1, source: this.getSource(event.body.source) } : undefined; + + if (event.body.group === 'start' || event.body.group === 'startCollapsed') { + const expanded = event.body.group === 'start'; + this.repl.startGroup(event.body.output || '', expanded, source); + return; + } + if (event.body.group === 'end') { + this.repl.endGroup(); + // Do not return, the end event can have additional output in it + } + if (event.body.variablesReference) { const container = new ExpressionContainer(this, undefined, event.body.variablesReference, generateUuid()); outpuPromises.push(container.getChildren().then(async children => { diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 8677590347cad..338d74cc9514c 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -51,12 +51,13 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; -import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider } from 'vs/workbench/contrib/debug/browser/replViewer'; +import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider, ReplGroupRenderer } from 'vs/workbench/contrib/debug/browser/replViewer'; import { localize } from 'vs/nls'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; const $ = dom.$; @@ -425,6 +426,16 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; await this.tree.updateChildren(); + + const session = this.tree.getInput(); + if (session) { + const replElements = session.getReplElements(); + const lastElement = replElements.length ? replElements[replElements.length - 1] : undefined; + if (lastElement instanceof ReplGroup && lastElement.autoExpand) { + await this.tree.expand(lastElement); + } + } + if (lastElementVisible) { // Only scroll if we were scrolled all the way down before tree refreshed #10486 revealLastElement(this.tree); @@ -454,6 +465,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), this.instantiationService.createInstance(ReplSimpleElementsRenderer, linkDetector), new ReplEvaluationInputsRenderer(), + new ReplGroupRenderer(), new ReplEvaluationResultsRenderer(linkDetector), new ReplRawObjectsRenderer(linkDetector), ], diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index de9cd9ce40bf4..53772e842addc 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -7,7 +7,7 @@ import severity from 'vs/base/common/severity'; import * as dom from 'vs/base/browser/dom'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; +import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -30,6 +30,10 @@ interface IReplEvaluationInputTemplateData { label: HighlightedLabel; } +interface IReplGroupTemplateData { + label: HighlightedLabel; +} + interface IReplEvaluationResultTemplateData { value: HTMLElement; annotation: HTMLElement; @@ -76,6 +80,29 @@ export class ReplEvaluationInputsRenderer implements ITreeRenderer { + static readonly ID = 'replGroup'; + + get templateId(): string { + return ReplGroupRenderer.ID; + } + + renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { + const input = dom.append(container, $('.expression')); + const label = new HighlightedLabel(input, false); + return { label }; + } + + renderElement(element: ITreeNode, _index: number, templateData: IReplGroupTemplateData): void { + const replGroup = element.element; + templateData.label.set(replGroup.name, createMatches(element.filterData)); + } + + disposeTemplate(_templateData: IReplEvaluationInputTemplateData): void { + // noop + } +} + export class ReplEvaluationResultsRenderer implements ITreeRenderer { static readonly ID = 'replEvaluationResult'; @@ -296,6 +323,9 @@ export class ReplDelegate extends CachedListVirtualDelegate { // Variable with no name is a top level variable which should be rendered like a repl element #17404 return ReplSimpleElementsRenderer.ID; } + if (element instanceof ReplGroup) { + return ReplGroupRenderer.ID; + } return ReplRawObjectsRenderer.ID; } @@ -317,7 +347,7 @@ export class ReplDataSource implements IAsyncDataSourceelement).hasChildren; + return !!(element).hasChildren; } getChildren(element: IReplElement | IDebugSession): Promise { @@ -327,6 +357,9 @@ export class ReplDataSource implements IAsyncDataSourceelement).getChildren(); } @@ -343,6 +376,9 @@ export class ReplAccessibilityProvider implements IAccessibilityProvider(); @@ -162,11 +216,29 @@ export class ReplModel { } } + startGroup(name: string, autoExpand: boolean, sourceData?: IReplElementSource): void { + const group = new ReplGroup(name, autoExpand, sourceData); + this.addReplElement(group); + } + + endGroup(): void { + const lastElement = this.replElements[this.replElements.length - 1]; + if (lastElement instanceof ReplGroup) { + lastElement.end(); + } + } + private addReplElement(newElement: IReplElement): void { - this.replElements.push(newElement); - if (this.replElements.length > MAX_REPL_LENGTH) { - this.replElements.splice(0, this.replElements.length - MAX_REPL_LENGTH); + const lastElement = this.replElements.length ? this.replElements[this.replElements.length - 1] : undefined; + if (lastElement instanceof ReplGroup && !lastElement.hasEnded) { + lastElement.addChild(newElement); + } else { + this.replElements.push(newElement); + if (this.replElements.length > MAX_REPL_LENGTH) { + this.replElements.splice(0, this.replElements.length - MAX_REPL_LENGTH); + } } + this._onDidChangeElements.fire(); } diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 297496be06471..1356848adc34c 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import severity from 'vs/base/common/severity'; import { DebugModel, StackFrame, Thread } from 'vs/workbench/contrib/debug/common/debugModel'; import { MockRawSession, MockDebugAdapter } from 'vs/workbench/contrib/debug/test/common/mockDebug'; -import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; +import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel, ReplEvaluationResult, ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; import { timeout } from 'vs/base/common/async'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; @@ -151,4 +151,42 @@ suite('Debug - REPL', () => { assert.equal((session.getReplElements()[4]).value, '=after.2'); assert.equal((session.getReplElements()[5]).value, 'after.2'); }); + + test('repl groups', async () => { + const session = createMockSession(model); + const repl = new ReplModel(); + + repl.appendToRepl(session, 'first global line', severity.Info); + repl.startGroup('group_1', true); + repl.appendToRepl(session, 'first line in group', severity.Info); + repl.appendToRepl(session, 'second line in group', severity.Info); + const elements = repl.getReplElements(); + assert.equal(elements.length, 2); + const group = elements[1] as ReplGroup; + assert.equal(group.name, 'group_1'); + assert.equal(group.autoExpand, true); + assert.equal(group.hasChildren, true); + assert.equal(group.hasEnded, false); + + repl.startGroup('group_2', false); + repl.appendToRepl(session, 'first line in subgroup', severity.Info); + repl.appendToRepl(session, 'second line in subgroup', severity.Info); + const children = group.getChildren(); + assert.equal(children.length, 3); + assert.equal((children[0]).value, 'first line in group'); + assert.equal((children[1]).value, 'second line in group'); + assert.equal((children[2]).name, 'group_2'); + assert.equal((children[2]).hasEnded, false); + assert.equal((children[2]).getChildren().length, 2); + repl.endGroup(); + assert.equal((children[2]).hasEnded, true); + repl.appendToRepl(session, 'third line in group', severity.Info); + assert.equal(group.getChildren().length, 4); + assert.equal(group.hasEnded, false); + repl.endGroup(); + assert.equal(group.hasEnded, true); + repl.appendToRepl(session, 'second global line', severity.Info); + assert.equal(repl.getReplElements().length, 3); + assert.equal((repl.getReplElements()[2]).value, 'second global line'); + }); });