Skip to content

Commit 387bab6

Browse files
authored
testing: add test actions to editor context menus (microsoft#167091)
* testing: add test actions to editor context menus Fixes microsoft#130548 * add an efficient test by uri lookup, and editor context key
1 parent afe8a34 commit 387bab6

File tree

9 files changed

+195
-94
lines changed

9 files changed

+195
-94
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"search.exclude": {
1919
"**/node_modules": true,
2020
"**/bower_components": true,
21+
"cli/target/**": true,
2122
".build/**": true,
2223
"out/**": true,
2324
"out-build/**": true,

src/vs/workbench/api/common/extHostTesting.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ interface MirroredCollectionTestItem extends IncrementalTestCollectionItem {
727727
depth: number;
728728
}
729729

730-
class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollectionTestItem> {
730+
class MirroredChangeCollector implements IncrementalChangeCollector<MirroredCollectionTestItem> {
731731
private readonly added = new Set<MirroredCollectionTestItem>();
732732
private readonly updated = new Set<MirroredCollectionTestItem>();
733733
private readonly removed = new Set<MirroredCollectionTestItem>();
@@ -739,30 +739,29 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
739739
}
740740

741741
constructor(private readonly emitter: Emitter<vscode.TestsChangeEvent>) {
742-
super();
743742
}
744743

745744
/**
746-
* @override
745+
* @inheritdoc
747746
*/
748-
public override add(node: MirroredCollectionTestItem): void {
747+
public add(node: MirroredCollectionTestItem): void {
749748
this.added.add(node);
750749
}
751750

752751
/**
753-
* @override
752+
* @inheritdoc
754753
*/
755-
public override update(node: MirroredCollectionTestItem): void {
754+
public update(node: MirroredCollectionTestItem): void {
756755
Object.assign(node.revived, Convert.TestItem.toPlain(node.item));
757756
if (!this.added.has(node)) {
758757
this.updated.add(node);
759758
}
760759
}
761760

762761
/**
763-
* @override
762+
* @inheritdoc
764763
*/
765-
public override remove(node: MirroredCollectionTestItem): void {
764+
public remove(node: MirroredCollectionTestItem): void {
766765
if (this.added.has(node)) {
767766
this.added.delete(node);
768767
return;
@@ -780,7 +779,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
780779
}
781780

782781
/**
783-
* @override
782+
* @inheritdoc
784783
*/
785784
public getChangeEvent(): vscode.TestsChangeEvent {
786785
const { added, updated, removed } = this;
@@ -791,7 +790,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
791790
};
792791
}
793792

794-
public override complete() {
793+
public complete() {
795794
if (!this.isEmpty) {
796795
this.emitter.fire(this.getChangeEvent());
797796
}

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
4141
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
4242
import { getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration';
4343
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
44+
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
45+
import { isCodeEditor } from 'vs/editor/browser/editorBrowser';
4446

4547
const category = Categories.Test;
4648

@@ -669,10 +671,15 @@ abstract class ExecuteTestAtCursor extends Action2 {
669671
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
670672
super({
671673
...options,
672-
menu: {
674+
menu: [{
673675
id: MenuId.CommandPalette,
674676
when: hasAnyTestProvider,
675-
},
677+
}, {
678+
id: MenuId.EditorContext,
679+
group: 'testing',
680+
order: group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug,
681+
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
682+
}]
676683
});
677684
}
678685

@@ -749,6 +756,8 @@ abstract class ExecuteTestAtCursor extends Action2 {
749756
group: this.group,
750757
tests: bestNodes.length ? bestNodes : bestNodesBefore,
751758
});
759+
} else if (isCodeEditor(activeControl)) {
760+
MessageController.get(activeControl)?.showMessage(localize('noTestsAtCursor', "No tests found here"), position);
752761
}
753762
}
754763
}
@@ -787,10 +796,16 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
787796
constructor(options: IAction2Options, protected readonly group: TestRunProfileBitset) {
788797
super({
789798
...options,
790-
menu: {
799+
menu: [{
791800
id: MenuId.CommandPalette,
792801
when: TestingContextKeys.capabilityToContextKey[group].isEqualTo(true),
793-
},
802+
}, {
803+
id: MenuId.EditorContext,
804+
group: 'testing',
805+
// add 0.1 to be after the "at cursor" commands
806+
order: (group === TestRunProfileBitset.Run ? ActionOrder.Run : ActionOrder.Debug) + 0.1,
807+
when: ContextKeyExpr.and(TestingContextKeys.activeEditorHasTests, TestingContextKeys.capabilityToContextKey[group]),
808+
}],
794809
});
795810
}
796811

@@ -830,6 +845,10 @@ abstract class ExecuteTestsInCurrentFile extends Action2 {
830845
});
831846
}
832847

848+
if (isCodeEditor(control)) {
849+
MessageController.get(control)?.showMessage(localize('noTestsInFile', "No tests found in this file"), position);
850+
}
851+
833852
return undefined;
834853
}
835854
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ export class TestingDecorationService extends Disposable implements ITestingDeco
200200
const map = model.changeDecorations(accessor => {
201201
const newDecorations: ITestDecoration[] = [];
202202
const runDecorations = new TestDecorations<{ line: number; id: ''; test: IncrementalTestCollectionItem; resultItem: TestResultItem | undefined }>();
203-
for (const test of this.testService.collection.all) {
204-
if (!test.item.range || test.item.uri?.toString() !== uriStr) {
203+
for (const test of this.testService.collection.getNodeByUrl(model.uri)) {
204+
if (!test.item.range) {
205205
continue;
206206
}
207207

src/vs/workbench/contrib/testing/common/mainThreadTestCollection.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55

66
import { Emitter } from 'vs/base/common/event';
77
import { Iterable } from 'vs/base/common/iterator';
8-
import { AbstractIncrementalTestCollection, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
8+
import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testTypes';
99
import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService';
10+
import { ResourceMap } from 'vs/base/common/map';
11+
import { URI } from 'vs/base/common/uri';
1012

1113
export class MainThreadTestCollection extends AbstractIncrementalTestCollection<IncrementalTestCollectionItem> implements IMainThreadTestCollection {
14+
private testsByUrl = new ResourceMap<Set<IncrementalTestCollectionItem>>();
15+
1216
private busyProvidersChangeEmitter = new Emitter<number>();
1317
private expandPromises = new WeakMap<IncrementalTestCollectionItem, {
1418
pendingLvl: number;
@@ -78,6 +82,13 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
7882
return this.items.get(id);
7983
}
8084

85+
/**
86+
* @inheritdoc
87+
*/
88+
public getNodeByUrl(uri: URI): Iterable<IncrementalTestCollectionItem> {
89+
return this.testsByUrl.get(uri) || Iterable.empty();
90+
}
91+
8192
/**
8293
* @inheritdoc
8394
*/
@@ -138,6 +149,40 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection<
138149
return { ...internal, children: new Set() };
139150
}
140151

152+
private readonly changeCollector: IncrementalChangeCollector<IncrementalTestCollectionItem> = {
153+
add: node => {
154+
if (!node.item.uri) {
155+
return;
156+
}
157+
158+
const s = this.testsByUrl.get(node.item.uri);
159+
if (!s) {
160+
this.testsByUrl.set(node.item.uri, new Set([node]));
161+
} else {
162+
s.add(node);
163+
}
164+
},
165+
remove: node => {
166+
if (!node.item.uri) {
167+
return;
168+
}
169+
170+
const s = this.testsByUrl.get(node.item.uri);
171+
if (!s) {
172+
return;
173+
}
174+
175+
s.delete(node);
176+
if (s.size === 0) {
177+
this.testsByUrl.delete(node.item.uri);
178+
}
179+
},
180+
};
181+
182+
protected override createChangeCollector(): IncrementalChangeCollector<IncrementalTestCollectionItem> {
183+
return this.changeCollector;
184+
}
185+
141186
private *getIterator() {
142187
const queue = [this.rootIds];
143188
while (queue.length) {

src/vs/workbench/contrib/testing/common/testService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface IMainThreadTestCollection extends AbstractIncrementalTestCollec
6161
*/
6262
getNodeById(id: string): IncrementalTestCollectionItem | undefined;
6363

64+
/**
65+
* Gets all tests that have the given URL. Tests returned from this
66+
* method are *not* in any particular order.
67+
*/
68+
getNodeByUrl(uri: URI): Iterable<IncrementalTestCollectionItem>;
69+
6470
/**
6571
* Requests that children be revealed for the given test. "Levels" may
6672
* be infinite.

src/vs/workbench/contrib/testing/common/testServiceImpl.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export class TestService extends Disposable implements ITestService {
4141
private readonly providerCount: IContextKey<number>;
4242
private readonly canRefreshTests: IContextKey<boolean>;
4343
private readonly isRefreshingTests: IContextKey<boolean>;
44+
private readonly activeEditorHasTests: IContextKey<boolean>;
45+
4446
/**
4547
* Cancellation for runs requested by the user being managed by the UI.
4648
* Test runs initiated by extensions are not included here.
@@ -97,6 +99,9 @@ export class TestService extends Disposable implements ITestService {
9799
this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService);
98100
this.canRefreshTests = TestingContextKeys.canRefreshTests.bindTo(contextKeyService);
99101
this.isRefreshingTests = TestingContextKeys.isRefreshingTests.bindTo(contextKeyService);
102+
this.activeEditorHasTests = TestingContextKeys.activeEditorHasTests.bindTo(contextKeyService);
103+
104+
this._register(editorService.onDidActiveEditorChange(() => this.updateEditorContextKeys()));
100105
}
101106

102107
/**
@@ -228,6 +233,7 @@ export class TestService extends Disposable implements ITestService {
228233
public publishDiff(_controllerId: string, diff: TestsDiff) {
229234
this.willProcessDiffEmitter.fire(diff);
230235
this.collection.apply(diff);
236+
this.updateEditorContextKeys();
231237
this.didProcessDiffEmitter.fire(diff);
232238
}
233239

@@ -313,6 +319,15 @@ export class TestService extends Disposable implements ITestService {
313319
return disposable;
314320
}
315321

322+
private updateEditorContextKeys() {
323+
const uri = this.editorService.activeEditor?.resource;
324+
if (uri) {
325+
this.activeEditorHasTests.set(!Iterable.isEmpty(this.collection.getNodeByUrl(uri)));
326+
} else {
327+
this.activeEditorHasTests.set(false);
328+
}
329+
}
330+
316331
private async saveAllBeforeTest(req: ResolvedTestRunRequest, configurationService: IConfigurationService = this.configurationService, editorService: IEditorService = this.editorService): Promise<void> {
317332
if (req.isUiTriggered === false) {
318333
return;

0 commit comments

Comments
 (0)