Skip to content

Commit

Permalink
testing: call stack tpi refinements
Browse files Browse the repository at this point in the history
Fixes #226855 - ctrl+click on words in files takes you to the source
Fixes #226863 - clicking file titles should toggle collapse
Fixes #226857 - add 'collapse all' button in peek
Fixes #226852 - name 'go to source' actions better
  • Loading branch information
connor4312 committed Aug 28, 2024
1 parent 9e6bd42 commit 40e586c
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 37 deletions.
124 changes: 101 additions & 23 deletions src/vs/workbench/contrib/debug/browser/callStackWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue } from 'vs/base/common/observable';
import { autorun, autorunWithStore, derived, IObservable, ISettableObservable, observableValue, transaction } from 'vs/base/common/observable';
import { ThemeIcon } from 'vs/base/common/themables';
import { Constants } from 'vs/base/common/uint';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import 'vs/css!./media/callStackWidget';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { EditorContributionCtor, EditorContributionInstantiation, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions';
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/codeEditorWidget';
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/codeEditor/embeddedCodeEditorWidget';
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IWordAtPosition } from 'vs/editor/common/core/wordHelper';
import { IEditorContribution, IEditorDecorationsCollection } from 'vs/editor/common/editorCommon';
import { Location } from 'vs/editor/common/languages';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture';
import { localize, localize2 } from 'vs/nls';
import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar';
Expand All @@ -38,7 +42,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { makeStackFrameColumnDecoration, TOP_STACK_FRAME_DECORATION } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';


export class CallStackFrame {
Expand Down Expand Up @@ -97,6 +101,9 @@ class WrappedCustomStackFrame implements IFrameLikeItem {
constructor(public readonly original: CustomStackFrame) { }
}

const isFrameLike = (item: unknown): item is IFrameLikeItem =>
item instanceof WrappedCallStackFrame || item instanceof WrappedCustomStackFrame;

type ListItem = WrappedCallStackFrame | SkippedCallFrames | WrappedCustomStackFrame;

const WIDGET_CLASS_NAME = 'multiCallStackWidget';
Expand Down Expand Up @@ -157,6 +164,17 @@ export class CallStackWidget extends Disposable {
this.layoutEmitter.fire();
}

public collapseAll() {
transaction(tx => {
for (let i = 0; i < this.list.length; i++) {
const frame = this.list.element(i);
if (isFrameLike(frame)) {
frame.collapsed.set(true, tx);
}
}
});
}

private async loadFrame(replacing: SkippedCallFrames): Promise<void> {
if (!this.cts) {
return;
Expand Down Expand Up @@ -356,9 +374,9 @@ abstract class AbstractFrameRenderer<T extends IAbstractFrameRendererTemplateDat
collapse.element.ariaExpanded = String(!collapsed);
elements.root.classList.toggle('collapsed', collapsed);
}));
elementStore.add(collapse.onDidClick(() => {
item.collapsed.set(!item.collapsed.get(), undefined);
}));
const toggleCollapse = () => item.collapsed.set(!item.collapsed.get(), undefined);
elementStore.add(collapse.onDidClick(toggleCollapse));
elementStore.add(dom.addDisposableListener(elements.title, 'click', toggleCollapse));
}

disposeElement(element: ListItem, index: number, templateData: T, height: number | undefined): void {
Expand All @@ -382,26 +400,33 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
private readonly containingEditor: ICodeEditor | undefined,
private readonly onLayout: Event<void>,
@ITextModelService private readonly modelService: ITextModelService,
@ICodeEditorService private readonly editorService: ICodeEditorService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super(instantiationService);
}

protected override finishRenderTemplate(data: IAbstractFrameRendererTemplateData): IStackTemplateData {
// override default e.g. language contributions, only allow users to click
// on code in the call stack to go to its source location
const contributions: IEditorContributionDescription[] = [{
id: ClickToLocationContribution.ID,
instantiation: EditorContributionInstantiation.BeforeFirstInteraction,
ctor: ClickToLocationContribution as EditorContributionCtor,
}];

const editor = this.containingEditor
? this.instantiationService.createInstance(
EmbeddedCodeEditorWidget,
data.elements.editor,
editorOptions,
{ isSimpleWidget: true },
{ isSimpleWidget: true, contributions },
this.containingEditor,
)
: this.instantiationService.createInstance(
CodeEditorWidget,
data.elements.editor,
editorOptions,
{ isSimpleWidget: true },
{ isSimpleWidget: true, contributions },
);

data.templateStore.add(editor);
Expand All @@ -423,20 +448,6 @@ class FrameCodeRenderer extends AbstractFrameRenderer<IStackTemplateData> {
const uri = item.source!;

template.label.element.setFile(uri);
template.elements.title.role = 'link';
elementStore.add(dom.addDisposableListener(template.elements.title, 'click', e => {
this.editorService.openCodeEditor({
resource: uri,
options: {
selection: Range.fromPositions({
column: item.column ?? 1,
lineNumber: item.line ?? 1,
}),
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
},
}, this.containingEditor || null, e.ctrlKey || e.metaKey);
}));

const cts = new CancellationTokenSource();
elementStore.add(toDisposable(() => cts.dispose(true)));
this.modelService.createModelReference(uri).then(reference => {
Expand Down Expand Up @@ -632,6 +643,73 @@ class SkippedRenderer implements IListRenderer<ListItem, ISkippedTemplateData> {
}
}

/** A simple contribution that makes all data in the editor clickable to go to the location */
class ClickToLocationContribution extends Disposable implements IEditorContribution {
public static readonly ID = 'clickToLocation';
private readonly linkDecorations: IEditorDecorationsCollection;
private current: { line: number; word: IWordAtPosition } | undefined;

constructor(
private readonly editor: ICodeEditor,
@IEditorService editorService: IEditorService,
) {
super();
this.linkDecorations = editor.createDecorationsCollection();
this._register(toDisposable(() => this.linkDecorations.clear()));

const clickLinkGesture = this._register(new ClickLinkGesture(editor));

this._register(clickLinkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
this.onMove(mouseEvent);
}));
this._register(clickLinkGesture.onExecute((e) => {
const model = this.editor.getModel();
if (!this.current || !model) {
return;
}

editorService.openEditor({
resource: model.uri,
options: {
selection: Range.fromPositions(new Position(this.current.line, this.current.word.startColumn)),
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
},
}, e.hasSideBySideModifier ? SIDE_GROUP : undefined);
}));
}

private onMove(mouseEvent: ClickLinkMouseEvent) {
if (!mouseEvent.hasTriggerModifier) {
return this.clear();
}

const position = mouseEvent.target.position;
const word = position && this.editor.getModel()?.getWordAtPosition(position);
if (!word) {
return this.clear();
}

const prev = this.current?.word;
if (prev && prev.startColumn === word.startColumn && prev.endColumn === word.endColumn && prev.word === word.word) {
return;
}

this.current = { word, line: position.lineNumber };
this.linkDecorations.set([{
range: new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
options: {
description: 'call-stack-go-to-file-link',
inlineClassName: 'call-stack-go-to-file-link',
},
}]);
}

private clear() {
this.linkDecorations.clear();
this.current = undefined;
}
}

registerAction2(class extends Action2 {
constructor() {
super({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,9 @@
line-height: inherit !important;
}
}

.monaco-editor .call-stack-go-to-file-link {
text-decoration: underline;
cursor: pointer;
color: var(--vscode-editorLink-activeForeground) !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export class TestResultStackWidget extends Disposable {
));
}

public collapseAll() {
this.widget.collapseAll();
}

public update(messageFrame: AnyStackFrame, stack: ITestMessageStackFrame[]) {
this.widget.setFrames([messageFrame, ...stack.map(frame => new CallStackFrame(
frame.label,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ interface ISubjectCommon {
controllerId: string;
}

export const inspectSubjectHasStack = (subject: InspectSubject | undefined) =>
subject instanceof MessageSubject && !!subject.stack?.length;

export class MessageSubject implements ISubjectCommon {
public readonly test: ITestItem;
public readonly message: ITestMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -838,22 +838,21 @@ class TreeActionsProvider {
}

if (element instanceof TestMessageElement) {
id = MenuId.TestMessageContext;
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);

primary.push(new Action(
'testing.outputPeek.goToFile',
localize('testing.goToFile', "Go to Source"),
'testing.outputPeek.goToTest',
localize('testing.goToTest', "Go to Test"),
ThemeIcon.asClassName(Codicon.goToFile),
undefined,
() => this.commandService.executeCommand('vscode.revealTest', element.test.item.extId),
));
}

if (element instanceof TestMessageElement) {
id = MenuId.TestMessageContext;
contextKeys.push([TestingContextKeys.testMessageContext.key, element.contextValue]);
if (this.showRevealLocationOnMessages && element.location) {
primary.push(new Action(
'testing.outputPeek.goToError',
localize('testing.goToError', "Go to Source"),
localize('testing.goToError', "Go to Error"),
ThemeIcon.asClassName(Codicon.goToFile),
undefined,
() => this.editorService.openEditor({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,13 @@ export class TestResultsViewContent extends Disposable {
});
}

/**
* Collapses all displayed stack frames.
*/
public collapseStack() {
this.callStackWidget.collapseAll();
}

private getCallFrames(subject: InspectSubject) {
if (!(subject instanceof MessageSubject)) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { testingResultsIcon, testingViewIcon } from 'vs/workbench/contrib/testin
import { TestCoverageView } from 'vs/workbench/contrib/testing/browser/testCoverageView';
import { TestingDecorationService, TestingDecorations } from 'vs/workbench/contrib/testing/browser/testingDecorations';
import { TestingExplorerView } from 'vs/workbench/contrib/testing/browser/testingExplorerView';
import { CloseTestPeek, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { CloseTestPeek, CollapsePeekStack, GoToNextMessageAction, GoToPreviousMessageAction, OpenMessageInEditorAction, TestResultsView, TestingOutputPeekController, TestingPeekOpener, ToggleTestingPeekHistory } from 'vs/workbench/contrib/testing/browser/testingOutputPeek';
import { TestingProgressTrigger } from 'vs/workbench/contrib/testing/browser/testingProgressUiService';
import { TestingViewPaneContainer } from 'vs/workbench/contrib/testing/browser/testingViewPaneContainer';
import { testingConfiguration } from 'vs/workbench/contrib/testing/common/configuration';
Expand Down Expand Up @@ -136,6 +136,7 @@ registerAction2(GoToPreviousMessageAction);
registerAction2(GoToNextMessageAction);
registerAction2(CloseTestPeek);
registerAction2(ToggleTestingPeekHistory);
registerAction2(CollapsePeekStack);

Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingContentProvider, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(TestingPeekOpener, LifecyclePhase.Eventually);
Expand Down
Loading

0 comments on commit 40e586c

Please sign in to comment.