Change:
<!-- Do Not Delete This! featurerequesttemplate --<!-- Please read our Rules of Conduct: --<!-- Please search existing issues to avoid creating duplicates. --Splitting this out from In the Test UI you can filter by text or exclude text using and filter by a . It would be nice if we could also negate tags in the filter like to exclude a specific tag. <!-- Describe the feature you'd like. --
Starting point, this is where tag filtering happens:

Discussion:
} private testTags(element: TestItemTreeElement): FilterResult { <del> if (!this.state.onlyTags.size) { </del> <ins> if (!this.state.includeTags.size && !this.state.excludeTags.size) { </ins> return FilterResult.Include; } <del> return element.test.item.tags.some(t => this.state.onlyTags.has(t)) </del> <ins> return (this.state.includeTags.size ? element.test.item.tags.some(t => this.state.includeTags.has(t)) : true) && element.test.item.tags.every(t => !this.state.excludeTags.has(t)) </ins> ? FilterResult.Include : FilterResult.Inherit; }

readonly globList: readonly { include: boolean; text: string }[]; /** <del> * The user requested to filter for only the specified tags. </del> <ins> * The user requested to filter including tags. </ins> */ <del> readonly onlyTags: ReadonlySet<string>; </del> <ins> readonly includeTags: ReadonlySet<string>; /** * The user requested to filter excluding tags. */ readonly excludeTags: ReadonlySet<string>; </ins> /** * Focuses the filter input in the test explorer view.

export const ITestExplorerFilterState = createDecorator<ITestExplorerFilterState>('testingFilterState'); <del> const tagRe = /@([^ ,:]+)/g; </del> <ins> const tagRe = /!?@([^ ,:]+)/g; </ins> const trimExtraWhitespace = (str: string) => str.replace(/ss+/g, ' ').trim(); export class TestExplorerFilterState implements ITestExplorerFilterState {

public globList: { include: boolean; text: string }[] = []; /** @inheritdoc */ <del> public onlyTags = new Set<string>(); </del> <ins> public includeTags = new Set<string>(); /** @inheritdoc */ public excludeTags = new Set<string>(); </ins> /** @inheritdoc */ public readonly text = new MutableObservableValue('');

this.termFilterState = {}; this.globList = []; <del> this.onlyTags.clear(); </del> <ins> this.includeTags.clear(); this.excludeTags.clear(); </ins> let globText = ''; let lastIndex = 0;

} } <del> this.onlyTags.add(TestTag.namespace(match[1], tagId)); </del> <ins> if (match[0].startsWith('!')) { this.excludeTags.add(TestTag.namespace(match[1], tagId)); } else { this.includeTags.add(TestTag.namespace(match[1], tagId)); } </ins> nextIndex++; }

test('filters simple globs', () => { t.setText('hello, !world'); assert.deepStrictEqual(t.globList, [{ text: 'hello', include: true }, { text: 'world', include: false }]); <del> assert.deepStrictEqual(t.onlyTags, new Set()); </del> <ins> assert.deepStrictEqual(t.includeTags, new Set()); assert.deepStrictEqual(t.excludeTags, new Set()); </ins> assertFilteringFor(termFiltersOff); }); test('filters to patterns', () => { t.setText('@doc'); assert.deepStrictEqual(t.globList, []); <del> assert.deepStrictEqual(t.onlyTags, new Set()); </del> <ins> assert.deepStrictEqual(t.includeTags, new Set()); assert.deepStrictEqual(t.excludeTags, new Set()); </ins> assertFilteringFor({ ...termFiltersOff, [TestFilterTerm.CurrentDoc]: true,

}); test('filters to tags', () => { <del> t.setText('@hello:world'); </del> <ins> t.setText('@hello:world !@foo:bar'); </ins> assert.deepStrictEqual(t.globList, []); <del> assert.deepStrictEqual(t.onlyTags, new Set(['hello0world'])); </del> <ins> assert.deepStrictEqual(t.includeTags, new Set(['hello0world'])); assert.deepStrictEqual(t.excludeTags, new Set(['foo0bar'])); </ins> assertFilteringFor(termFiltersOff); }); test('filters to mixed terms and tags', () => { <del> t.setText('@hello:world foo, !bar @doc'); </del> <ins> t.setText('@hello:world foo, !bar @doc !@foo:bar'); </ins> assert.deepStrictEqual(t.globList, [{ text: 'foo', include: true }, { text: 'bar', include: false }]); <del> assert.deepStrictEqual(t.onlyTags, new Set(['hello0world'])); </del> <ins> assert.deepStrictEqual(t.includeTags, new Set(['hello0world'])); assert.deepStrictEqual(t.excludeTags, new Set(['foo0bar'])); </ins> assertFilteringFor({ ...termFiltersOff, [TestFilterTerm.CurrentDoc]: true,

test('parses quotes', () => { t.setText('@hello:"world" @foo:'bar' baz'); assert.deepStrictEqual(t.globList, [{ text: 'baz', include: true }]); <del> assert.deepStrictEqual([...t.onlyTags], ['hello0world', 'foo0bar']); </del> <ins> assert.deepStrictEqual([...t.includeTags], ['hello0world', 'foo0bar']); assert.deepStrictEqual(t.excludeTags, new Set()); </ins> }); test('parses quotes with escapes', () => { t.setText('@hello:"world"1" foo'); assert.deepStrictEqual(t.globList, [{ text: 'foo', include: true }]); <del> assert.deepStrictEqual([...t.onlyTags], ['hello0world"1']); </del> <ins> assert.deepStrictEqual([...t.includeTags], ['hello0world"1']); assert.deepStrictEqual(t.excludeTags, new Set()); </ins> }); });