Skip to content

Commit 0c51035

Browse files
authored
testing: fix errors peeks being oversized relative to their contents (microsoft#236763)
Fixes microsoft#214011
1 parent 3751af2 commit 0c51035

File tree

6 files changed

+85
-95
lines changed

6 files changed

+85
-95
lines changed

src/vs/base/browser/ui/list/listView.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,8 @@ export class ListView<T> implements IListView<T> {
567567

568568
if (this.supportDynamicHeights) {
569569
this._rerender(this.lastRenderTop, this.lastRenderHeight);
570+
} else {
571+
this._onDidChangeContentHeight.fire(this.contentHeight); // otherwise fired in _rerender()
570572
}
571573
}
572574

src/vs/editor/contrib/zoneWidget/browser/zoneWidget.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ export interface IOptions {
2929
frameColor?: Color | string;
3030
arrowColor?: Color;
3131
keepEditorSelection?: boolean;
32-
allowUnlimitedHeight?: boolean;
3332
ordinal?: number;
3433
showInHiddenAreas?: boolean;
3534
}
@@ -376,6 +375,11 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
376375
return result;
377376
}
378377

378+
/** Gets the maximum widget height in lines. */
379+
protected _getMaximumHeightInLines(): number | undefined {
380+
return Math.max(12, (this.editor.getLayoutInfo().height / this.editor.getOption(EditorOption.lineHeight)) * 0.8);
381+
}
382+
379383
private _showImpl(where: Range, heightInLines: number): void {
380384
const position = where.getStartPosition();
381385
const layoutInfo = this.editor.getLayoutInfo();
@@ -389,8 +393,8 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
389393
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
390394

391395
// adjust heightInLines to viewport
392-
if (!this.options.allowUnlimitedHeight) {
393-
const maxHeightInLines = Math.max(12, (this.editor.getLayoutInfo().height / lineHeight) * 0.8);
396+
const maxHeightInLines = this._getMaximumHeightInLines();
397+
if (maxHeightInLines !== undefined) {
394398
heightInLines = Math.min(heightInLines, maxHeightInLines);
395399
}
396400

@@ -493,7 +497,9 @@ export abstract class ZoneWidget implements IHorizontalSashLayoutProvider {
493497
// implement in subclass
494498
}
495499

496-
protected _relayout(newHeightInLines: number): void {
500+
protected _relayout(_newHeightInLines: number): void {
501+
const maxHeightInLines = this._getMaximumHeightInLines();
502+
const newHeightInLines = maxHeightInLines === undefined ? _newHeightInLines : Math.min(maxHeightInLines, _newHeightInLines);
497503
if (this._viewZone && this._viewZone.heightInLines !== newHeightInLines) {
498504
this.editor.changeViewZones(accessor => {
499505
if (this._viewZone) {

src/vs/workbench/contrib/debug/browser/callStackWidget.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,18 @@ export class CallStackWidget extends Disposable {
119119
private readonly currentFramesDs = this._register(new DisposableStore());
120120
private cts?: CancellationTokenSource;
121121

122+
public get onDidChangeContentHeight() {
123+
return this.list.onDidChangeContentHeight;
124+
}
125+
122126
public get onDidScroll() {
123127
return this.list.onDidScroll;
124128
}
125129

130+
public get contentHeight() {
131+
return this.list.contentHeight;
132+
}
133+
126134
constructor(
127135
container: HTMLElement,
128136
containingEditor: ICodeEditor | undefined,

src/vs/workbench/contrib/testing/browser/testResultsView/testMessageStack.ts

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/vs/workbench/contrib/testing/browser/testResultsView/testResultsViewContent.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { Emitter, Event, Relay } from '../../../../../base/common/event.js';
1414
import { KeyCode } from '../../../../../base/common/keyCodes.js';
1515
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
1616
import { observableValue } from '../../../../../base/common/observable.js';
17-
import './testResultsViewContent.css';
1817
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
1918
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
2019
import { localize } from '../../../../../nls.js';
@@ -28,19 +27,19 @@ import { IInstantiationService, ServicesAccessor } from '../../../../../platform
2827
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
2928
import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';
3029
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
31-
import { CustomStackFrame } from '../../../debug/browser/callStackWidget.js';
32-
import * as icons from '../icons.js';
33-
import { TestResultStackWidget } from './testMessageStack.js';
34-
import { DiffContentProvider, IPeekOutputRenderer, MarkdownTestMessagePeek, PlainTextMessagePeek, TerminalMessagePeek } from './testResultsOutput.js';
35-
import { equalsSubject, getSubjectTestItem, InspectSubject, MessageSubject, TaskSubject, TestOutputSubject } from './testResultsSubject.js';
36-
import { OutputPeekTree } from './testResultsTree.js';
30+
import { AnyStackFrame, CallStackFrame, CallStackWidget, CustomStackFrame } from '../../../debug/browser/callStackWidget.js';
3731
import { TestCommandId } from '../../common/constants.js';
3832
import { IObservableValue } from '../../common/observableValue.js';
3933
import { capabilityContextKeys, ITestProfileService } from '../../common/testProfileService.js';
4034
import { LiveTestResult } from '../../common/testResult.js';
4135
import { ITestFollowup, ITestService } from '../../common/testService.js';
4236
import { ITestMessageStackFrame, TestRunProfileBitset } from '../../common/testTypes.js';
4337
import { TestingContextKeys } from '../../common/testingContextKeys.js';
38+
import * as icons from '../icons.js';
39+
import { DiffContentProvider, IPeekOutputRenderer, MarkdownTestMessagePeek, PlainTextMessagePeek, TerminalMessagePeek } from './testResultsOutput.js';
40+
import { equalsSubject, getSubjectTestItem, InspectSubject, MessageSubject, TaskSubject, TestOutputSubject } from './testResultsSubject.js';
41+
import { OutputPeekTree } from './testResultsTree.js';
42+
import './testResultsViewContent.css';
4443

4544
const enum SubView {
4645
Diff = 0,
@@ -183,7 +182,7 @@ export class TestResultsViewContent extends Disposable {
183182
private contextKeyTestMessage!: IContextKey<string>;
184183
private contextKeyResultOutdated!: IContextKey<boolean>;
185184
private stackContainer!: HTMLElement;
186-
private callStackWidget!: TestResultStackWidget;
185+
private callStackWidget!: CallStackWidget;
187186
private currentTopFrame?: MessageStackFrame;
188187
private isDoingLayoutUpdate?: boolean;
189188

@@ -209,6 +208,14 @@ export class TestResultsViewContent extends Disposable {
209208
};
210209
}
211210

211+
public get onDidChangeContentHeight() {
212+
return this.callStackWidget.onDidChangeContentHeight;
213+
}
214+
215+
public get contentHeight() {
216+
return this.callStackWidget?.contentHeight || 0;
217+
}
218+
212219
constructor(
213220
private readonly editor: ICodeEditor | undefined,
214221
private readonly options: {
@@ -233,7 +240,7 @@ export class TestResultsViewContent extends Disposable {
233240

234241
const messageContainer = this.messageContainer = dom.$('.test-output-peek-message-container');
235242
this.stackContainer = dom.append(containerElement, dom.$('.test-output-call-stack-container'));
236-
this.callStackWidget = this._register(this.instantiationService.createInstance(TestResultStackWidget, this.stackContainer, this.editor));
243+
this.callStackWidget = this._register(this.instantiationService.createInstance(CallStackWidget, this.stackContainer, this.editor));
237244
this.followupWidget = this._register(this.instantiationService.createInstance(FollowupActionWidget, this.editor));
238245
this.onCloseEmitter.input = this.followupWidget.onClose;
239246

@@ -315,13 +322,22 @@ export class TestResultsViewContent extends Disposable {
315322
this.currentSubjectStore.clear();
316323
const callFrames = this.getCallFrames(opts.subject) || [];
317324
const topFrame = await this.prepareTopFrame(opts.subject, callFrames);
318-
this.callStackWidget.update(topFrame, callFrames);
325+
this.setCallStackFrames(topFrame, callFrames);
319326

320327
this.followupWidget.show(opts.subject);
321328
this.populateFloatingClick(opts.subject);
322329
});
323330
}
324331

332+
private setCallStackFrames(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) {
333+
this.callStackWidget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame(
334+
frame.label,
335+
frame.uri,
336+
frame.position?.lineNumber,
337+
frame.position?.column,
338+
))]);
339+
}
340+
325341
/**
326342
* Collapses all displayed stack frames.
327343
*/
@@ -375,7 +391,6 @@ export class TestResultsViewContent extends Disposable {
375391
}));
376392
}
377393

378-
379394
if (provider.onDidContentSizeChange) {
380395
this.currentSubjectStore.add(provider.onDidContentSizeChange(() => {
381396
if (this.dimension && !this.isDoingLayoutUpdate) {

src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
import * as dom from '../../../../base/browser/dom.js';
77
import { alert } from '../../../../base/browser/ui/aria/aria.js';
88
import { IAction } from '../../../../base/common/actions.js';
9+
import { RunOnceScheduler } from '../../../../base/common/async.js';
910
import { Codicon } from '../../../../base/common/codicons.js';
1011
import { Color } from '../../../../base/common/color.js';
11-
import { Emitter, Event } from '../../../../base/common/event.js';
12+
import { Event } from '../../../../base/common/event.js';
1213
import { stripIcons } from '../../../../base/common/iconLabels.js';
1314
import { Iterable } from '../../../../base/common/iterator.js';
1415
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
1516
import { Lazy } from '../../../../base/common/lazy.js';
1617
import { Disposable } from '../../../../base/common/lifecycle.js';
1718
import { derived, disposableObservableValue, observableValue } from '../../../../base/common/observable.js';
18-
import { count } from '../../../../base/common/strings.js';
1919
import { URI } from '../../../../base/common/uri.js';
2020
import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
2121
import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js';
@@ -678,10 +678,8 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo
678678

679679

680680
class TestResultsPeek extends PeekViewWidget {
681-
private static lastHeightInLines?: number;
682-
683-
private readonly visibilityChange = this._disposables.add(new Emitter<boolean>());
684681
public readonly current = observableValue<InspectSubject | undefined>('testPeekCurrent', undefined);
682+
private resizeOnNextContentHeightUpdate = false;
685683
private content!: TestResultsViewContent;
686684
private scopedContextKeyService!: IContextKeyService;
687685
private dimension?: dom.Dimension;
@@ -701,10 +699,22 @@ class TestResultsPeek extends PeekViewWidget {
701699
super(editor, { showFrame: true, frameWidth: 1, showArrow: true, isResizeable: true, isAccessible: true, className: 'test-output-peek' }, instantiationService);
702700

703701
this._disposables.add(themeService.onDidColorThemeChange(this.applyTheme, this));
704-
this._disposables.add(this.onDidClose(() => this.visibilityChange.fire(false)));
705702
peekViewService.addExclusiveWidget(editor, this);
706703
}
707704

705+
protected override _getMaximumHeightInLines(): number | undefined {
706+
const defaultMaxHeight = super._getMaximumHeightInLines();
707+
const contentHeight = this.content?.contentHeight;
708+
if (!contentHeight) { // undefined or 0
709+
return defaultMaxHeight;
710+
}
711+
712+
const lineHeight = this.editor.getOption(EditorOption.lineHeight);
713+
// 41 is experimentally determined to be the overhead of the peek view itself
714+
// to avoid showing scrollbars by default in its content.
715+
return Math.min(defaultMaxHeight || Infinity, (contentHeight + 41) / lineHeight);
716+
}
717+
708718
private applyTheme() {
709719
const theme = this.themeService.getColorTheme();
710720
const current = this.current.get();
@@ -764,6 +774,27 @@ class TestResultsPeek extends PeekViewWidget {
764774

765775
protected override _fillBody(containerElement: HTMLElement): void {
766776
this.content.fillBody(containerElement);
777+
778+
// Resize on height updates for a short time to allow any heights made
779+
// by editor contributions to come into effect before.
780+
const contentHeightSettleTimer = this._disposables.add(new RunOnceScheduler(() => {
781+
this.resizeOnNextContentHeightUpdate = false;
782+
}, 500));
783+
784+
this._disposables.add(this.content.onDidChangeContentHeight(height => {
785+
if (!this.resizeOnNextContentHeightUpdate || !height) {
786+
return;
787+
}
788+
789+
const displayed = this._getMaximumHeightInLines();
790+
if (displayed) {
791+
this._relayout(Math.min(displayed, this.getVisibleEditorLines() / 2));
792+
if (!contentHeightSettleTimer.isScheduled()) {
793+
contentHeightSettleTimer.schedule();
794+
}
795+
}
796+
}));
797+
767798
this._disposables.add(this.content.onDidRequestReveal(sub => {
768799
TestingOutputPeekController.get(this.editor)?.show(sub instanceof MessageSubject
769800
? sub.messageUri
@@ -780,7 +811,6 @@ class TestResultsPeek extends PeekViewWidget {
780811
return this.showInPlace(subject);
781812
}
782813

783-
const message = subject.message;
784814
const previous = this.current;
785815
const revealLocation = subject.revealLocation?.range.getStartPosition();
786816
if (!revealLocation && !previous) {
@@ -792,13 +822,8 @@ class TestResultsPeek extends PeekViewWidget {
792822
return this.showInPlace(subject);
793823
}
794824

795-
// If there is a stack we want to display, ensure the default size is large-ish
796-
const peekLines = TestResultsPeek.lastHeightInLines || Math.max(
797-
inspectSubjectHasStack(subject) ? Math.ceil(this.getVisibleEditorLines() / 2) : 0,
798-
hintMessagePeekHeight(message)
799-
);
800-
801-
this.show(revealLocation, peekLines);
825+
this.resizeOnNextContentHeightUpdate = true;
826+
this.show(revealLocation, 10); // 10 is just a random number, we resize once content is available
802827
this.editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(revealLocation), ScrollType.Smooth);
803828

804829
return this.showInPlace(subject);
@@ -832,11 +857,6 @@ class TestResultsPeek extends PeekViewWidget {
832857
await this.content.reveal({ subject, preserveFocus: false });
833858
}
834859

835-
protected override _relayout(newHeightInLines: number): void {
836-
super._relayout(newHeightInLines);
837-
TestResultsPeek.lastHeightInLines = newHeightInLines;
838-
}
839-
840860
/** @override */
841861
protected override _doLayoutBody(height: number, width: number) {
842862
super._doLayoutBody(height, width);
@@ -919,23 +939,11 @@ export class TestResultsView extends ViewPane {
919939
}
920940
}
921941

922-
const hintMessagePeekHeight = (msg: ITestMessage) => {
923-
const msgHeight = ITestMessage.isDiffable(msg)
924-
? Math.max(hintPeekStrHeight(msg.actual), hintPeekStrHeight(msg.expected))
925-
: hintPeekStrHeight(typeof msg.message === 'string' ? msg.message : msg.message.value);
926-
927-
// add 8ish lines for the size of the title and decorations in the peek.
928-
return msgHeight + 8;
929-
};
930-
931942
const firstLine = (str: string) => {
932943
const index = str.indexOf('\n');
933944
return index === -1 ? str : str.slice(0, index);
934945
};
935946

936-
937-
const hintPeekStrHeight = (str: string) => Math.min(count(str, '\n'), 24);
938-
939947
function getOuterEditorFromDiffEditor(codeEditorService: ICodeEditorService): ICodeEditor | null {
940948
const diffEditors = codeEditorService.listDiffEditors();
941949

0 commit comments

Comments
 (0)