Skip to content

Commit

Permalink
testing: add test actions to editor context menus (microsoft#167091)
Browse files Browse the repository at this point in the history
* testing: add test actions to editor context menus

Fixes microsoft#130548

* add an efficient test by uri lookup, and editor context key
  • Loading branch information
connor4312 authored Nov 23, 2022
1 parent afe8a34 commit 387bab6
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 94 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"cli/target/**": true,
".build/**": true,
"out/**": true,
"out-build/**": true,
Expand Down
19 changes: 9 additions & 10 deletions src/vs/workbench/api/common/extHostTesting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ interface MirroredCollectionTestItem extends IncrementalTestCollectionItem {
depth: number;
}

class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollectionTestItem> {
class MirroredChangeCollector implements IncrementalChangeCollector<MirroredCollectionTestItem> {
private readonly added = new Set<MirroredCollectionTestItem>();
private readonly updated = new Set<MirroredCollectionTestItem>();
private readonly removed = new Set<MirroredCollectionTestItem>();
Expand All @@ -739,30 +739,29 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
}

constructor(private readonly emitter: Emitter<vscode.TestsChangeEvent>) {
super();
}

/**
* @override
* @inheritdoc
*/
public override add(node: MirroredCollectionTestItem): void {
public add(node: MirroredCollectionTestItem): void {
this.added.add(node);
}

/**
* @override
* @inheritdoc
*/
public override update(node: MirroredCollectionTestItem): void {
public update(node: MirroredCollectionTestItem): void {
Object.assign(node.revived, Convert.TestItem.toPlain(node.item));
if (!this.added.has(node)) {
this.updated.add(node);
}
}

/**
* @override
* @inheritdoc
*/
public override remove(node: MirroredCollectionTestItem): void {
public remove(node: MirroredCollectionTestItem): void {
if (this.added.has(node)) {
this.added.delete(node);
return;
Expand All @@ -780,7 +779,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
}

/**
* @override
* @inheritdoc
*/
public getChangeEvent(): vscode.TestsChangeEvent {
const { added, updated, removed } = this;
Expand All @@ -791,7 +790,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
};
}

public override complete() {
public complete() {
if (!this.isEmpty) {
this.emitter.fire(this.getChangeEvent());
}
Expand Down
27 changes: 23 additions & 4 deletions src/vs/workbench/contrib/testing/browser/testExplorerActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';

const category = Categories.Test;

Expand Down Expand Up @@ -669,10 +671,15 @@ abstract class ExecuteTestAtCursor extends Action2 {
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
super({
...options,
menu: {
menu: [{
id: MenuId.CommandPalette,
when: hasAnyTestProvider,
},
}, {
id: MenuId.EditorContext,
group: 'testing',
order: group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug,
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
}]
});
}

Expand Down Expand Up @@ -749,6 +756,8 @@ abstract class ExecuteTestAtCursor extends Action2 {
group: this.group,
tests: bestNodes.length ? bestNodes : bestNodesBefore,
});
} else if (isCodeEditor(activeControl)) {
MessageController.get(activeControl)?.showMessage(localize('noTestsAtCursor', "No tests found here"), position);
}
}
}
Expand Down Expand Up @@ -787,10 +796,16 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
super({
...options,
menu: {
menu: [{
id: MenuId.CommandPalette,
when: TestingContextKeys.capabilityToContextKey[group].isEqualTo(true),
},
}, {
id: MenuId.EditorContext,
group: 'testing',
// add 0.1 to be after the "at cursor" commands
order: (group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug) + 0.1,
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
}],
});
}

Expand Down Expand Up @@ -830,6 +845,10 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
});
}

if (isCodeEditor(control)) {
MessageController.get(control)?.showMessage(localize('noTestsInFile', "No tests found in this file"), position);
}

return undefined;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ export class TestingDecorationService extends Disposable implements ITestingDeco
const map = model.changeDecorations(accessor => {
const newDecorations: ITestDecoration[] = [];
const runDecorations = new TestDecorations<{ line: number; id: ''; test: IncrementalTestCollectionItem; resultItem: TestResultItem | undefined }>();
for (const test of this.testService.collection.all) {
if (!test.item.range || test.item.uri?.toString() !== uriStr) {
for (const test of this.testService.collection.getNodeByUrl(model.uri)) {
if (!test.item.range) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

import { Emitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
import { ResourceMap } from 'vs/base/common/map';
import { URI } from 'vs/base/common/uri';

export class MainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> implements IMainThreadTestCollection {
private testsByUrl = new ResourceMap<Set<IncrementalTestCollectionItem>>();

private busyProvidersChangeEmitter = new Emitter<number>();
private expandPromises = new WeakMap<IncrementalTestCollectionItem, {
pendingLvl: number;
Expand Down Expand Up @@ -78,6 +82,13 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
return this.items.get(id);
}

/**
* @inheritdoc
*/
public getNodeByUrl(uri: URI): Iterable<IncrementalTestCollectionItem> {
return this.testsByUrl.get(uri) || Iterable.empty();
}

/**
* @inheritdoc
*/
Expand Down Expand Up @@ -138,6 +149,40 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
return { ...internal, children: new Set() };
}

private readonly changeCollector: IncrementalChangeCollector<IncrementalTestCollectionItem> = {
add: node => {
if (!node.item.uri) {
return;
}

const s = this.testsByUrl.get(node.item.uri);
if (!s) {
this.testsByUrl.set(node.item.uri, new Set([node]));
} else {
s.add(node);
}
},
remove: node => {
if (!node.item.uri) {
return;
}

const s = this.testsByUrl.get(node.item.uri);
if (!s) {
return;
}

s.delete(node);
if (s.size === 0) {
this.testsByUrl.delete(node.item.uri);
}
},
};

protected override createChangeCollector(): IncrementalChangeCollector<IncrementalTestCollectionItem> {
return this.changeCollector;
}

private *getIterator() {
const queue = [this.rootIds];
while (queue.length) {
Expand Down
6 changes: 6 additions & 0 deletions src/vs/workbench/contrib/testing/common/testService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export interface IMainThreadTestCollection extends AbstractIncrementalTestCollec
*/
getNodeById(id: string): IncrementalTestCollectionItem | undefined;

/**
* Gets all tests that have the given URL. Tests returned from this
* method are *not* in any particular order.
*/
getNodeByUrl(uri: URI): Iterable<IncrementalTestCollectionItem>;

/**
* Requests that children be revealed for the given test. "Levels" may
* be infinite.
Expand Down
15 changes: 15 additions & 0 deletions src/vs/workbench/contrib/testing/common/testServiceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export class TestService extends Disposable implements ITestService {
private readonly providerCount: IContextKey<number>;
private readonly canRefreshTests: IContextKey<boolean>;
private readonly isRefreshingTests: IContextKey<boolean>;
private readonly activeEditorHasTests: IContextKey<boolean>;

/**
* Cancellation for runs requested by the user being managed by the UI.
* Test runs initiated by extensions are not included here.
Expand Down Expand Up @@ -97,6 +99,9 @@ export class TestService extends Disposable implements ITestService {
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
this.canRefreshTests = TestingContextKeys.canRefreshTests.bindTo(contextKeyService);
this.isRefreshingTests = TestingContextKeys.isRefreshingTests.bindTo(contextKeyService);
this.activeEditorHasTests = TestingContextKeys.activeEditorHasTests.bindTo(contextKeyService);

this._register(editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys()));
}

/**
Expand Down Expand Up @@ -228,6 +233,7 @@ export class TestService extends Disposable implements ITestService {
public publishDiff(_controllerId: string, diff: TestsDiff) {
this.willProcessDiffEmitter.fire(diff);
this.collection.apply(diff);
this.updateEditorContextKeys();
this.didProcessDiffEmitter.fire(diff);
}

Expand Down Expand Up @@ -313,6 +319,15 @@ export class TestService extends Disposable implements ITestService {
return disposable;
}

private updateEditorContextKeys() {
const uri = this.editorService.activeEditor?.resource;
if (uri) {
this.activeEditorHasTests.set(!Iterable.isEmpty(this.collection.getNodeByUrl(uri)));
} else {
this.activeEditorHasTests.set(false);
}
}

private async saveAllBeforeTest(req: ResolvedTestRunRequest, configurationService: IConfigurationService = this.configurationService, editorService: IEditorService = this.editorService): Promise<void> {
if (req.isUiTriggered === false) {
return;
Expand Down
Loading

0 comments on commit 387bab6

Please sign in to comment.