testing: polish followups a little, selfhost with copilot

This commit is contained in:
Connor Peet 2024-05-22 13:08:51 -07:00
parent 4ebc77f80a
commit 6193553bfe
No known key found for this signature in database
GPG key ID: CF8FD2EA0DBC61BD
6 changed files with 86 additions and 29 deletions

View file

@ -41,16 +41,15 @@ export async function activate(context: vscode.ExtensionContext) {
const ctrl = vscode.tests.createTestController('selfhost-test-controller', 'VS Code Tests');
const fileChangedEmitter = new vscode.EventEmitter<FileChangeEvent>();
// todo@connor4312: tidy this up and make it work
// context.subscriptions.push(vscode.tests.registerTestFollowupProvider({
// async provideFollowup(result, test, taskIndex, messageIndex, token) {
// await new Promise(r => setTimeout(r, 2000));
// return [{
// title: '$(sparkle) Ask copilot for help',
// command: 'asdf'
// }];
// },
// }));
context.subscriptions.push(vscode.tests.registerTestFollowupProvider({
async provideFollowup(_result, test, taskIndex, messageIndex, _token) {
return [{
title: '$(sparkle) Ask copilot for help',
command: 'github.copilot.tests.fixTestFailure',
arguments: [{ source: 'peekFollowup', test, message: test.taskStates[taskIndex].messages[messageIndex] }]
}];
},
}));
ctrl.resolveHandler = async test => {

View file

@ -44,6 +44,12 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
super();
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
this._register(this.testService.registerExtHost({
provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token),
executeTestFollowup: id => this.proxy.$executeTestFollowup(id),
disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids),
}));
this._register(this.testService.onDidCancelTestRun(({ runId }) => {
this.proxy.$cancelExtensionTestRun(runId);
}));
@ -233,9 +239,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
runTests: (reqs, token) => this.proxy.$runControllerTests(reqs, token),
startContinuousRun: (reqs, token) => this.proxy.$startContinuousRun(reqs, token),
expandTest: (testId, levels) => this.proxy.$expandTest(testId, isFinite(levels) ? levels : -1),
provideTestFollowups: (req, token) => this.proxy.$provideTestFollowups(req, token),
executeTestFollowup: id => this.proxy.$executeTestFollowup(id),
disposeTestFollowups: ids => this.proxy.$disposeTestFollowups(ids),
};
disposable.add(toDisposable(() => this.testProfiles.removeProfile(controllerId)));

View file

@ -277,6 +277,9 @@
overflow: hidden;
pointer-events: none;
background: linear-gradient(transparent, var(--vscode-peekViewEditor-background) 50%);
display: flex;
align-items: center;
gap: 14px;
&.animated {
animation: fadeIn 150ms ease-out;
@ -289,6 +292,7 @@
cursor: pointer;
pointer-events: auto;
width: fit-content;
flex-shrink: 0;
&, .codicon {
color: var(--vscode-textLink-foreground);

View file

@ -69,6 +69,7 @@ import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listSe
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
@ -780,6 +781,7 @@ class FollowupActionWidget extends Disposable {
constructor(
private readonly container: HTMLElement,
@ITestService private readonly testService: ITestService,
@IQuickInputService private readonly quickInput: IQuickInputService,
) {
super();
}
@ -794,6 +796,12 @@ class FollowupActionWidget extends Disposable {
private async showMessage(subject: MessageSubject) {
const cts = this.visibleStore.add(new CancellationTokenSource());
const start = Date.now();
// Wait for completion otherwise results will not be available to the ext host:
if (subject.result instanceof LiveTestResult && !subject.result.completedAt) {
await new Promise(r => Event.once((subject.result as LiveTestResult).onComplete)(r));
}
const followups = await this.testService.provideTestFollowups({
extId: subject.test.extId,
messageIndex: subject.messageIndex,
@ -811,20 +819,10 @@ class FollowupActionWidget extends Disposable {
dom.clearNode(this.el.root);
this.el.root.classList.toggle('animated', Date.now() - start > FOLLOWUP_ANIMATION_MIN_TIME);
for (const fu of followups.followups) {
const link = document.createElement('a');
link.tabIndex = 0;
dom.reset(link, ...renderLabelWithIcons(fu.message));
this.visibleStore.add(dom.addDisposableListener(link, 'click', () => this.actionFollowup(link, fu)));
this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
this.actionFollowup(link, fu);
}
}));
this.el.root.appendChild(link);
this.el.root.appendChild(this.makeFollowupLink(followups.followups[0]));
if (followups.followups.length > 1) {
this.el.root.appendChild(this.makeMoreLink(followups.followups));
}
this.container.appendChild(this.el.root);
@ -833,6 +831,42 @@ class FollowupActionWidget extends Disposable {
}));
}
private makeFollowupLink(first: ITestFollowup) {
const link = this.makeLink(() => this.actionFollowup(link, first));
dom.reset(link, ...renderLabelWithIcons(first.message));
return link;
}
private makeMoreLink(followups: ITestFollowup[]) {
const link = this.makeLink(() =>
this.quickInput.pick(followups.map((f, i) => ({
label: f.message,
index: i
}))).then(picked => {
if (picked?.length) {
followups[picked[0].index].execute();
}
})
);
link.innerText = localize('testFollowup.more', '+{0} More...', followups.length - 1);
return link;
}
private makeLink(onClick: () => void) {
const link = document.createElement('a');
link.tabIndex = 0;
this.visibleStore.add(dom.addDisposableListener(link, 'click', onClick));
this.visibleStore.add(dom.addDisposableListener(link, 'keydown', e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) {
onClick();
}
}));
return link;
}
private actionFollowup(link: HTMLAnchorElement, fu: ITestFollowup) {
if (link.ariaDisabled !== 'true') {
link.ariaDisabled = 'true';

View file

@ -29,6 +29,9 @@ export interface IMainThreadTestController {
expandTest(id: string, levels: number): Promise<void>;
startContinuousRun(request: ICallProfileRunHandler[], token: CancellationToken): Promise<IStartControllerTestsResult[]>;
runTests(request: IStartControllerTests[], token: CancellationToken): Promise<IStartControllerTestsResult[]>;
}
export interface IMainThreadTestHostProxy {
provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<TestMessageFollowupResponse[]>;
executeTestFollowup(id: number): Promise<void>;
disposeTestFollowups(ids: number[]): void;
@ -273,6 +276,11 @@ export interface ITestService {
*/
readonly showInlineOutput: MutableObservableValue<boolean>;
/**
* Registers an interface that represents an extension host..
*/
registerExtHost(controller: IMainThreadTestHostProxy): IDisposable;
/**
* Registers an interface that runs tests for the given provider ID.
*/

View file

@ -27,13 +27,14 @@ import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingC
import { canUseProfileWithTest, ITestProfileService } from 'vs/workbench/contrib/testing/common/testProfileService';
import { ITestResult } from 'vs/workbench/contrib/testing/common/testResult';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { AmbiguousRunTestsRequest, IMainThreadTestController, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { AmbiguousRunTestsRequest, IMainThreadTestController, IMainThreadTestHostProxy, ITestFollowups, ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ResolvedTestRunRequest, TestDiffOpType, TestMessageFollowupRequest, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
export class TestService extends Disposable implements ITestService {
declare readonly _serviceBrand: undefined;
private testControllers = new Map<string, IMainThreadTestController>();
private testExtHosts = new Set<IMainThreadTestHostProxy>();
private readonly cancelExtensionTestRunEmitter = new Emitter<{ runId: string | undefined }>();
private readonly willProcessDiffEmitter = new Emitter<TestsDiff>();
@ -268,8 +269,8 @@ export class TestService extends Disposable implements ITestService {
* @inheritdoc
*/
public async provideTestFollowups(req: TestMessageFollowupRequest, token: CancellationToken): Promise<ITestFollowups> {
const reqs = await Promise.all([...this.testControllers.values()]
.map(async ctrl => ({ ctrl, followups: await ctrl.provideTestFollowups(req, token) })));
const reqs = await Promise.all([...this.testExtHosts].map(async ctrl =>
({ ctrl, followups: await ctrl.provideTestFollowups(req, token) })));
const followups: ITestFollowups = {
followups: reqs.flatMap(({ ctrl, followups }) => followups.map(f => ({
@ -351,6 +352,14 @@ export class TestService extends Disposable implements ITestService {
this.isRefreshingTests.set(false);
}
/**
* @inheritdoc
*/
registerExtHost(controller: IMainThreadTestHostProxy): IDisposable {
this.testExtHosts.add(controller);
return toDisposable(() => this.testExtHosts.delete(controller));
}
/**
* @inheritdoc
*/