mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
testing: add filter box
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -78,5 +78,6 @@
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features",
|
||||
"editor.formatOnSave": true,
|
||||
},
|
||||
"typescript.format.semicolons": "insert",
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export class SequencerByKey<TKey> {
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to delay execution of a task that is being requested often.
|
||||
* A helper to delay (debounce) execution of a task that is being requested often.
|
||||
*
|
||||
* Following the throttler, now imagine the mail man wants to optimize the number of
|
||||
* trips proactively. The trip itself can be long, so he decides not to make the trip
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
import { ICompressedTreeElement } from 'vs/base/browser/ui/tree/compressedObjectTreeModel';
|
||||
import { CompressibleObjectTree, ObjectTree } from 'vs/base/browser/ui/tree/objectTree';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
|
||||
export const isRunningState = (s: TestRunState) => s === TestRunState.Queued || s === TestRunState.Running;
|
||||
|
||||
/**
|
||||
* Removes nodes from the set whose parents don't exist in the tree. This is
|
||||
* useful to remove nodes that are queued to be updated or rendered, who will
|
||||
@@ -24,7 +27,7 @@ export const pruneNodesWithParentsNotInTree = <T extends ITestTreeElement>(nodes
|
||||
/**
|
||||
* Helper to gather and bulk-apply tree updates.
|
||||
*/
|
||||
export class NodeChangeList<T extends ITestTreeElement & { children: Iterable<T>; parentItem: T | null }> {
|
||||
export class NodeChangeList<T extends ITestTreeElement & { children: Iterable<T>; parentItem: T | null; }> {
|
||||
private changedParents = new Set<T | null>();
|
||||
private updatedNodes = new Set<T>();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Location as ModeLocation } from 'vs/editor/common/modes';
|
||||
import { TestRunState } from 'vs/workbench/api/common/extHostTypes';
|
||||
import { ITestTreeElement, ITestTreeProjection } from 'vs/workbench/contrib/testing/browser/explorerProjections';
|
||||
import { locationsEqual, TestLocationStore } from 'vs/workbench/contrib/testing/browser/explorerProjections/locationStore';
|
||||
import { NodeChangeList } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { isRunningState, NodeChangeList } from 'vs/workbench/contrib/testing/browser/explorerProjections/nodeHelper';
|
||||
import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProjections/stateNodes';
|
||||
import { statesInOrder } from 'vs/workbench/contrib/testing/browser/testExplorerTree';
|
||||
import { TestSubscriptionListener } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
|
||||
@@ -187,9 +187,19 @@ export class StateByLocationProjection extends AbstractIncrementalTestCollection
|
||||
}
|
||||
},
|
||||
update: node => {
|
||||
const isRunning = isRunningState(node.item.state.runState);
|
||||
if (node.item.state.runState !== node.previousState) {
|
||||
this.pruneStateElements(node, node.previousState);
|
||||
this.resolveNodesRecursive(node);
|
||||
if (isRunning && node.treeElements.has(node.previousState)) {
|
||||
node.treeElements.get(node.previousState)!.computedState = TestRunState.Running;
|
||||
} else {
|
||||
this.pruneStateElements(node, node.previousState);
|
||||
this.resolveNodesRecursive(node);
|
||||
}
|
||||
} else if (!isRunning) {
|
||||
const previous = node.treeElements.get(node.item.state.runState);
|
||||
if (previous) {
|
||||
previous.computedState = node.item.state.runState;
|
||||
}
|
||||
}
|
||||
|
||||
const locationChanged = !locationsEqual(node.location, node.item.location);
|
||||
@@ -199,7 +209,7 @@ export class StateByLocationProjection extends AbstractIncrementalTestCollection
|
||||
this.locations.add(node);
|
||||
}
|
||||
|
||||
const treeNode = node.treeElements.get(node.item.state.runState)!;
|
||||
const treeNode = node.treeElements.get(node.previousState)!;
|
||||
this.changes.updated(treeNode);
|
||||
},
|
||||
complete: () => {
|
||||
|
||||
@@ -178,10 +178,15 @@ export class StateByNameProjection extends AbstractIncrementalTestCollection<ISt
|
||||
}
|
||||
},
|
||||
update: node => {
|
||||
if (node.item.state.runState !== node.previousState) {
|
||||
this.removeNode(node);
|
||||
if (node.item.state.runState !== node.previousState && node.node) {
|
||||
if (node.item.state.runState === TestRunState.Running) {
|
||||
node.node.computedState = node.item.state.runState;
|
||||
} else {
|
||||
this.removeNode(node);
|
||||
}
|
||||
}
|
||||
|
||||
node.previousState = node.item.state.runState;
|
||||
this.resolveNodesRecursive(node);
|
||||
|
||||
const locationChanged = !locationsEqual(node.location, node.item.location);
|
||||
|
||||
@@ -3,6 +3,21 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.test-explorer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.test-explorer > .monaco-inputbox {
|
||||
flex-shrink: 0;
|
||||
margin: 4px 12px;
|
||||
}
|
||||
|
||||
.test-explorer > .test-explorer-tree {
|
||||
flex-grow: 1;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.test-explorer .test-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { addStandardDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue';
|
||||
|
||||
export class TestingFilterState {
|
||||
private readonly changeEmitter = new Emitter<string>();
|
||||
|
||||
public readonly onDidChange = this.changeEmitter.event;
|
||||
|
||||
public get value() {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
public set value(v: string) {
|
||||
if (v !== this._value) {
|
||||
this._value = v;
|
||||
this.changeEmitter.fire(v);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private _value = '') { }
|
||||
}
|
||||
|
||||
export class TestingExplorerFilter extends Widget {
|
||||
private readonly input: HistoryInputBox;
|
||||
private readonly history: StoredValue<string[]> = this.instantiationService.createInstance(StoredValue, {
|
||||
key: 'testing.filterHistory',
|
||||
scope: StorageScope.WORKSPACE,
|
||||
target: StorageTarget.USER
|
||||
});
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
private readonly state: TestingFilterState,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
const updateDelayer = this._register(new Delayer<void>(400));
|
||||
|
||||
const input = this.input = this._register(instantiationService.createInstance(ContextScopedHistoryInputBox, container, contextViewService, {
|
||||
placeholder: localize('testExplorerFilter', "Filter (e.g. text, !exclude)"),
|
||||
history: this.history.get([]),
|
||||
}));
|
||||
input.value = state.value;
|
||||
this._register(attachInputBoxStyler(input, themeService));
|
||||
|
||||
this._register(state.onDidChange(newValue => {
|
||||
input.value = newValue;
|
||||
}));
|
||||
|
||||
this._register(input.onDidChange(() => updateDelayer.trigger(() => {
|
||||
input.addToHistory();
|
||||
this.state.value = input.value;
|
||||
})));
|
||||
|
||||
this._register(addStandardDisposableListener(input.inputElement, EventType.KEY_DOWN, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
input.value = '';
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Focuses the filter input.
|
||||
*/
|
||||
public focus(): void {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists changes to the input history.
|
||||
*/
|
||||
public saveState() {
|
||||
const history = this.input.getHistory();
|
||||
if (history.length) {
|
||||
this.history.store(history);
|
||||
} else {
|
||||
this.history.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { ITreeEvent, ITreeFilter, ITreeNode, ITreeSorter, TreeFilterResult, Tree
|
||||
import { throttle } from 'vs/base/common/decorators';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { splitGlobAware } from 'vs/base/common/glob';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./media/testing';
|
||||
@@ -49,6 +50,7 @@ import { StateElement } from 'vs/workbench/contrib/testing/browser/explorerProje
|
||||
import { testingStatesToIcons } from 'vs/workbench/contrib/testing/browser/icons';
|
||||
import { cmpPriority } from 'vs/workbench/contrib/testing/browser/testExplorerTree';
|
||||
import { ITestingCollectionService, TestSubscriptionListener } from 'vs/workbench/contrib/testing/browser/testingCollectionService';
|
||||
import { TestingExplorerFilter, TestingFilterState } from 'vs/workbench/contrib/testing/browser/testingExplorerFilter';
|
||||
import { TestExplorerViewGrouping, TestExplorerViewMode } from 'vs/workbench/contrib/testing/common/constants';
|
||||
import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys';
|
||||
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
|
||||
@@ -57,8 +59,10 @@ import { DebugAction, RunAction } from './testExplorerActions';
|
||||
|
||||
export class TestingExplorerView extends ViewPane {
|
||||
public viewModel!: TestingExplorerViewModel;
|
||||
private readonly filterState = new TestingFilterState();
|
||||
private filter!: TestingExplorerFilter;
|
||||
private currentSubscription?: TestSubscriptionListener;
|
||||
private listContainer!: HTMLElement;
|
||||
private container!: HTMLElement;
|
||||
private finishDiscovery?: () => void;
|
||||
|
||||
constructor(
|
||||
@@ -93,8 +97,12 @@ export class TestingExplorerView extends ViewPane {
|
||||
protected renderBody(container: HTMLElement): void {
|
||||
super.renderBody(container);
|
||||
|
||||
this.listContainer = dom.append(container, dom.$('.test-explorer'));
|
||||
this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, this.listContainer, this.onDidChangeBodyVisibility, this.currentSubscription);
|
||||
this.container = dom.append(container, dom.$('.test-explorer'));
|
||||
this.filter = this.instantiationService.createInstance(TestingExplorerFilter, this.container, this.filterState);
|
||||
this._register(this.filter);
|
||||
|
||||
const listContainer = dom.append(this.container, dom.$('.test-explorer-tree'));
|
||||
this.viewModel = this.instantiationService.createInstance(TestingExplorerViewModel, listContainer, this.onDidChangeBodyVisibility, this.currentSubscription, this.filterState);
|
||||
this._register(this.viewModel);
|
||||
|
||||
this.updateProgressIndicator();
|
||||
@@ -118,6 +126,14 @@ export class TestingExplorerView extends ViewPane {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
public saveState() {
|
||||
super.saveState();
|
||||
this.filter.saveState();
|
||||
}
|
||||
|
||||
private updateProgressIndicator() {
|
||||
const busy = Iterable.some(this.testService.busyTestLocations, s => s.resource === ExtHostTestingResource.Workspace);
|
||||
if (!busy && this.finishDiscovery) {
|
||||
@@ -134,7 +150,7 @@ export class TestingExplorerView extends ViewPane {
|
||||
*/
|
||||
protected layoutBody(height: number, width: number): void {
|
||||
super.layoutBody(height, width);
|
||||
this.listContainer.style.height = `${height}px`;
|
||||
this.container.style.height = `${height}px`;
|
||||
this.viewModel.layout(height, width);
|
||||
}
|
||||
|
||||
@@ -189,6 +205,7 @@ export class TestingExplorerViewModel extends Disposable {
|
||||
listContainer: HTMLElement,
|
||||
onDidChangeVisibility: Event<boolean>,
|
||||
private listener: TestSubscriptionListener | undefined,
|
||||
filterState: TestingFilterState,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IEditorService editorService: IEditorService,
|
||||
@ICodeEditorService codeEditorService: ICodeEditorService,
|
||||
@@ -202,7 +219,12 @@ export class TestingExplorerViewModel extends Disposable {
|
||||
|
||||
const labels = this._register(instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: onDidChangeVisibility }));
|
||||
|
||||
this.filter = new TestsFilter();
|
||||
this.filter = new TestsFilter(filterState.value);
|
||||
this._register(filterState.onDidChange(text => {
|
||||
this.filter.setFilter(text);
|
||||
this.tree.refilter();
|
||||
}));
|
||||
|
||||
this.tree = instantiationService.createInstance(
|
||||
WorkbenchCompressibleObjectTree,
|
||||
'Test Explorer List',
|
||||
@@ -375,30 +397,65 @@ class CodeEditorTracker {
|
||||
}
|
||||
}
|
||||
|
||||
class TestsFilter implements ITreeFilter<ITestTreeElement> {
|
||||
private filters: [include: boolean, value: string][] | undefined;
|
||||
|
||||
class TestsFilter implements ITreeFilter<ITestTreeElement, FuzzyScore> {
|
||||
private filterText: string | undefined;
|
||||
|
||||
public setFilter(filterText: string) {
|
||||
this.filterText = filterText;
|
||||
constructor(initialFilter: string) {
|
||||
this.setFilter(initialFilter);
|
||||
}
|
||||
|
||||
public filter(element: ITestTreeElement): TreeFilterResult<FuzzyScore> {
|
||||
/**
|
||||
* Parses and updates the tree filter. Supports lists of patterns that can be !negated.
|
||||
*/
|
||||
public setFilter(text: string) {
|
||||
text = text.trim();
|
||||
|
||||
if (!text) {
|
||||
this.filters = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.filters = [];
|
||||
for (const filter of splitGlobAware(text, ',').map(s => s.trim()).filter(s => !!s.length)) {
|
||||
if (filter.startsWith('!')) {
|
||||
this.filters.push([false, filter.slice(1).toLowerCase()]);
|
||||
} else {
|
||||
this.filters.push([true, filter.toLowerCase()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public filter(element: ITestTreeElement): TreeFilterResult<void> {
|
||||
if (element instanceof HierarchicalByNameElement && element.elementType !== ListElementType.TestLeaf && !element.isTestRoot) {
|
||||
return TreeVisibility.Hidden;
|
||||
}
|
||||
|
||||
if (!this.filterText) {
|
||||
if (this.testFilterText(element.label)) {
|
||||
return TreeVisibility.Visible;
|
||||
}
|
||||
|
||||
if (element.label.includes(this.filterText)) {
|
||||
return TreeVisibility.Visible;
|
||||
return Iterable.isEmpty(element.getChildren()) ? TreeVisibility.Hidden : TreeVisibility.Recurse;
|
||||
}
|
||||
|
||||
private testFilterText(data: string) {
|
||||
if (!this.filters) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return TreeVisibility.Recurse;
|
||||
// start as included if the first glob is a negation
|
||||
let included = this.filters[0][0] === false;
|
||||
data = data.toLowerCase();
|
||||
|
||||
for (const [include, filter] of this.filters) {
|
||||
if (data.includes(filter)) {
|
||||
included = include;
|
||||
}
|
||||
}
|
||||
|
||||
return included;
|
||||
}
|
||||
}
|
||||
|
||||
class TreeSorter implements ITreeSorter<ITestTreeElement> {
|
||||
public compare(a: ITestTreeElement, b: ITestTreeElement): number {
|
||||
if (a instanceof StateElement && b instanceof StateElement) {
|
||||
|
||||
79
src/vs/workbench/contrib/testing/common/storedValue.ts
Normal file
79
src/vs/workbench/contrib/testing/common/storedValue.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export interface IStoredValueSerialization<T> {
|
||||
deserialize(data: string): T;
|
||||
serialize(data: T): string;
|
||||
}
|
||||
|
||||
const defaultSerialization: IStoredValueSerialization<any> = {
|
||||
deserialize: d => JSON.parse(d),
|
||||
serialize: d => JSON.stringify(d),
|
||||
};
|
||||
|
||||
interface IStoredValueOptions<T> {
|
||||
key: string;
|
||||
scope: StorageScope;
|
||||
target: StorageTarget;
|
||||
serialization?: IStoredValueSerialization<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* todo@connor4312: is this worthy to be in common?
|
||||
*/
|
||||
export class StoredValue<T> {
|
||||
private readonly serialization: IStoredValueSerialization<T>;
|
||||
private readonly key: string;
|
||||
private readonly scope: StorageScope;
|
||||
private readonly target: StorageTarget;
|
||||
|
||||
/**
|
||||
* Emitted whenever the value is updated or deleted.
|
||||
*/
|
||||
public readonly onDidChange = Event.filter(this.storage.onDidChangeValue, e => e.key === this.key);
|
||||
|
||||
constructor(
|
||||
options: IStoredValueOptions<T>,
|
||||
@IStorageService private readonly storage: IStorageService,
|
||||
) {
|
||||
this.key = options.key;
|
||||
this.scope = options.scope;
|
||||
this.target = options.target;
|
||||
this.serialization = options.serialization ?? defaultSerialization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the value, returning the undefined if it's not set.
|
||||
*/
|
||||
public get(): T | undefined;
|
||||
|
||||
/**
|
||||
* Reads the value, returning the default value if it's not set.
|
||||
*/
|
||||
public get(defaultValue: T): T;
|
||||
|
||||
public get(defaultValue?: T): T | undefined {
|
||||
const value = this.storage.get(this.key, this.scope);
|
||||
return value === undefined ? defaultValue : this.serialization.deserialize(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists changes to the value.
|
||||
* @param value
|
||||
*/
|
||||
public store(value: T) {
|
||||
this.storage.store(this.key, this.serialization.serialize(value), this.scope, this.target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an element stored under the provided key from storage.
|
||||
*/
|
||||
public delete() {
|
||||
this.storage.remove(this.key, this.scope);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user