From 1c3ba58ccf3cac50a7132c945bf90fa13808b8b2 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Wed, 17 Dec 2025 17:02:54 -0800 Subject: [PATCH 1/4] Add glob pattern filter settings to snippets --- .../browser/commands/configureSnippets.ts | 19 ++++ .../browser/snippetCompletionProvider.ts | 2 +- .../snippets/browser/snippets.contribution.ts | 14 +++ .../contrib/snippets/browser/snippets.ts | 5 +- .../contrib/snippets/browser/snippetsFile.ts | 69 ++++++++++++++- .../snippets/browser/snippetsService.ts | 8 +- .../snippets/test/browser/snippetFile.test.ts | 86 +++++++++++++++++++ 7 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index 29f82cafa74b1..c6075dfeae732 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -192,6 +192,16 @@ async function createSnippetFile(scope: string, defaultPath: URI, quickInputServ '\t// \t],', '\t// \t"description": "Log output to console"', '\t// }', + '\t//', + '\t// You can also restrict snippets to specific files using include/exclude patterns:', + '\t// "Test snippet": {', + '\t// \t"scope": "javascript,typescript",', + '\t// \t"prefix": "test",', + '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', + '\t// \t"include": ["**/*.test.ts", "**/*.spec.ts"],', + '\t// \t"exclude": ["**/*.min.js"],', + '\t// \t"description": "Insert test block"', + '\t// }', '}' ].join('\n')); @@ -218,6 +228,15 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS '\t// \t],', '\t// \t"description": "Log output to console"', '\t// }', + '\t//', + '\t// You can also restrict snippets to specific files using include/exclude patterns:', + '\t// "Test snippet": {', + '\t// \t"prefix": "test",', + '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', + '\t// \t"include": ["**/*.test.ts", "**/*.spec.ts"],', + '\t// \t"exclude": ["**/*.min.js"],', + '\t// \t"description": "Insert test block"', + '\t// }', '}' ].join('\n'); await textFileService.write(pick.filepath, contents); diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 4bd9ab7344643..8e4f1d375b5db 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -105,7 +105,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const triggerCharacterLow = context.triggerCharacter?.toLowerCase() ?? ''; const languageId = this._getLanguageIdAtPosition(model, position); const languageConfig = this._languageConfigurationService.getLanguageConfiguration(languageId); - const snippets = new Set(await this._snippets.getSnippets(languageId)); + const snippets = new Set(await this._snippets.getSnippets(languageId, undefined, model.uri)); const suggestions: SnippetCompletion[] = []; for (const snippet of snippets) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 226fccbff8e0c..0266acd11d707 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -75,6 +75,20 @@ const snippetSchemaProperties: IJSONSchemaMap = { description: { description: nls.localize('snippetSchema.json.description', 'The snippet description.'), type: ['string', 'array'] + }, + include: { + markdownDescription: nls.localize('snippetSchema.json.include', 'A list of glob patterns to include the snippet for specific files, e.g. `["**/*.test.ts", "**/*.spec.ts"]` or `"**/travis.yml"`.'), + type: ['string', 'array'], + items: { + type: 'string' + } + }, + exclude: { + markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of glob patterns to exclude the snippet from specific files, e.g. `["**/*.min.js"]`.'), + type: ['string', 'array'], + items: { + type: 'string' + } } }; diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.ts b/src/vs/workbench/contrib/snippets/browser/snippets.ts index ae096afb2a6ec..67a65629e075c 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.ts @@ -5,6 +5,7 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { SnippetFile, Snippet } from './snippetsFile.js'; +import { URI } from '../../../../base/common/uri.js'; export const ISnippetsService = createDecorator('snippetService'); @@ -27,7 +28,7 @@ export interface ISnippetsService { updateUsageTimestamp(snippet: Snippet): void; - getSnippets(languageId: string | undefined, opt?: ISnippetGetOptions): Promise; + getSnippets(languageId: string | undefined, opt?: ISnippetGetOptions, resourceUri?: URI): Promise; - getSnippetsSync(languageId: string, opt?: ISnippetGetOptions): Snippet[]; + getSnippetsSync(languageId: string, opt?: ISnippetGetOptions, resourceUri?: URI): Snippet[]; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index fb981d5bbb75f..2a88df7acc293 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -16,6 +16,7 @@ import { relativePath } from '../../../../base/common/resources.js'; import { isObject } from '../../../../base/common/types.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { WindowIdleValue, getActiveWindow } from '../../../../base/browser/dom.js'; +import { parse as parseGlob, ParsedExpression, IExpression } from '../../../../base/common/glob.js'; class SnippetBodyInsights { @@ -100,6 +101,8 @@ class SnippetBodyInsights { export class Snippet { private readonly _bodyInsights: WindowIdleValue; + private _parsedInclude?: ParsedExpression; + private _parsedExclude?: ParsedExpression; readonly prefixLow: string; @@ -113,6 +116,8 @@ export class Snippet { readonly source: string, readonly snippetSource: SnippetSource, readonly snippetIdentifier: string, + readonly include?: string[], + readonly exclude?: string[], readonly extensionId?: ExtensionIdentifier, ) { this.prefixLow = prefix.toLowerCase(); @@ -138,6 +143,36 @@ export class Snippet { get usesSelection(): boolean { return this._bodyInsights.value.usesSelectionVariable; } + + isFileIncluded(resourceUri: URI): boolean { + const filePath = resourceUri.fsPath; + + if (this.exclude) { + if (!this._parsedExclude) { + const expression: IExpression = {}; + for (const pattern of this.exclude.filter(p => Boolean(p))) { + expression[pattern] = true; + } + this._parsedExclude = parseGlob(expression, { ignoreCase: true }); + } + if (this._parsedExclude(filePath)) { + return false; + } + } + + if (this.include) { + if (!this._parsedInclude) { + const expression: IExpression = {}; + for (const pattern of this.include.filter(p => Boolean(p))) { + expression[pattern] = true; + } + this._parsedInclude = parseGlob(expression, { ignoreCase: true }); + } + return !!this._parsedInclude(filePath); + } + + return true; + } } @@ -147,6 +182,8 @@ interface JsonSerializedSnippet { scope?: string; prefix: string | string[] | undefined; description: string; + include?: string | string[]; + exclude?: string | string[]; } function isJsonSerializedSnippet(thing: unknown): thing is JsonSerializedSnippet { @@ -183,9 +220,9 @@ export class SnippetFile { this.isUserSnippets = !this._extension; } - select(selector: string, bucket: Snippet[]): void { + select(selector: string, bucket: Snippet[], resourceUri?: URI): void { if (this.isGlobalSnippets || !this.isUserSnippets) { - this._scopeSelect(selector, bucket); + this._scopeSelect(selector, bucket, resourceUri); } else { this._filepathSelect(selector, bucket); } @@ -198,9 +235,13 @@ export class SnippetFile { } } - private _scopeSelect(selector: string, bucket: Snippet[]): void { + private _scopeSelect(selector: string, bucket: Snippet[], resourceUri?: URI): void { // for `my.code-snippets` files we need to look at each snippet for (const snippet of this.data) { + if (resourceUri && !snippet.isFileIncluded(resourceUri)) { + continue; + } + const len = snippet.scopes.length; if (len === 0) { // always accept @@ -219,7 +260,7 @@ export class SnippetFile { const idx = selector.lastIndexOf('.'); if (idx >= 0) { - this._scopeSelect(selector.substring(0, idx), bucket); + this._scopeSelect(selector.substring(0, idx), bucket, resourceUri); } } @@ -286,6 +327,24 @@ export class SnippetFile { scopes = []; } + let include: string[] | undefined; + if (snippet.include) { + if (Array.isArray(snippet.include)) { + include = snippet.include; + } else if (typeof snippet.include === 'string') { + include = [snippet.include]; + } + } + + let exclude: string[] | undefined; + if (snippet.exclude) { + if (Array.isArray(snippet.exclude)) { + exclude = snippet.exclude; + } else if (typeof snippet.exclude === 'string') { + exclude = [snippet.exclude]; + } + } + let source: string; if (this._extension) { // extension snippet -> show the name of the extension @@ -314,6 +373,8 @@ export class SnippetFile { source, this.source, this._extension ? `${relativePath(this._extension.extensionLocation, this.location)}/${name}` : `${basename(this.location.path)}/${name}`, + include, + exclude, this._extension?.identifier, )); } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index 187615df1deee..c13332bea4e59 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -265,7 +265,7 @@ export class SnippetsService implements ISnippetsService { return this._files.values(); } - async getSnippets(languageId: string | undefined, opts?: ISnippetGetOptions): Promise { + async getSnippets(languageId: string | undefined, opts?: ISnippetGetOptions, resourceUri?: URI): Promise { await this._joinSnippets(); const result: Snippet[] = []; @@ -275,7 +275,7 @@ export class SnippetsService implements ISnippetsService { if (this._languageService.isRegisteredLanguageId(languageId)) { for (const file of this._files.values()) { promises.push(file.load() - .then(file => file.select(languageId, result)) + .then(file => file.select(languageId, result, resourceUri)) .catch(err => this._logService.error(err, file.location.toString())) ); } @@ -292,14 +292,14 @@ export class SnippetsService implements ISnippetsService { return this._filterAndSortSnippets(result, opts); } - getSnippetsSync(languageId: string, opts?: ISnippetGetOptions): Snippet[] { + getSnippetsSync(languageId: string, opts?: ISnippetGetOptions, resourceUri?: URI): Snippet[] { const result: Snippet[] = []; if (this._languageService.isRegisteredLanguageId(languageId)) { for (const file of this._files.values()) { // kick off loading (which is a noop in case it's already loaded) // and optimistically collect snippets file.load().catch(_err => { /*ignore*/ }); - file.select(languageId, result); + file.select(languageId, result, resourceUri); } } return this._filterAndSortSnippets(result, opts); diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index 8274299f3da00..2fb299cc7538f 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -102,4 +102,90 @@ suite('Snippets', function () { assertIsTrivial('${1:foo}', false); }); + test('SnippetFile#select - include pattern', function () { + + const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ + new Snippet(false, ['typescript'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts']), + new Snippet(false, ['typescript'], 'SpecSnippet', 'spec', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.spec.ts']), + new Snippet(false, ['typescript'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Test file should only get TestSnippet and AllSnippet + let bucket: Snippet[] = []; + file.select('typescript', bucket, URI.file('/project/src/foo.test.ts')); + assert.strictEqual(bucket.length, 2); + assert.ok(bucket.some(s => s.name === 'TestSnippet')); + assert.ok(bucket.some(s => s.name === 'AllSnippet')); + + // Spec file should only get SpecSnippet and AllSnippet + bucket = []; + file.select('typescript', bucket, URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(bucket.length, 2); + assert.ok(bucket.some(s => s.name === 'SpecSnippet')); + assert.ok(bucket.some(s => s.name === 'AllSnippet')); + + // Regular file should only get AllSnippet + bucket = []; + file.select('typescript', bucket, URI.file('/project/src/foo.ts')); + assert.strictEqual(bucket.length, 1); + assert.strictEqual(bucket[0].name, 'AllSnippet'); + + // Without URI, all snippets should be selected (backward compatibility) + bucket = []; + file.select('typescript', bucket); + assert.strictEqual(bucket.length, 3); + }); + + test('SnippetFile#select - exclude pattern', function () { + + const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ + new Snippet(false, ['javascript'], 'ProdSnippet', 'prod', '', 'snippet', 'test', SnippetSource.User, generateUuid(), undefined, ['**/*.min.js', '**/dist/**']), + new Snippet(false, ['javascript'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Regular .js file should get both snippets + let bucket: Snippet[] = []; + file.select('javascript', bucket, URI.file('/project/src/foo.js')); + assert.strictEqual(bucket.length, 2); + + // Minified file should only get AllSnippet (ProdSnippet is excluded) + bucket = []; + file.select('javascript', bucket, URI.file('/project/src/foo.min.js')); + assert.strictEqual(bucket.length, 1); + assert.strictEqual(bucket[0].name, 'AllSnippet'); + + // File in dist folder should only get AllSnippet + bucket = []; + file.select('javascript', bucket, URI.file('/project/dist/bundle.js')); + assert.strictEqual(bucket.length, 1); + assert.strictEqual(bucket[0].name, 'AllSnippet'); + }); + + test('SnippetFile#select - include and exclude patterns together', function () { + + const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ + new Snippet(false, ['typescript'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts', '**/*.spec.ts'], ['**/*.perf.test.ts']), + ]); + + // Regular test file should get the snippet + let bucket: Snippet[] = []; + file.select('typescript', bucket, URI.file('/project/src/foo.test.ts')); + assert.strictEqual(bucket.length, 1); + + // Spec file should get the snippet + bucket = []; + file.select('typescript', bucket, URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(bucket.length, 1); + + // Performance test file should NOT get the snippet (excluded) + bucket = []; + file.select('typescript', bucket, URI.file('/project/src/foo.perf.test.ts')); + assert.strictEqual(bucket.length, 0); + + // Regular file should NOT get the snippet (not included) + bucket = []; + file.select('typescript', bucket, URI.file('/project/src/foo.ts')); + assert.strictEqual(bucket.length, 0); + }); + }); From c629c9f7524e3825b5c6aaeb84fd0817da557cd8 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Mon, 5 Jan 2026 17:03:28 +0100 Subject: [PATCH 2/4] PR feedback --- .../browser/commands/fileTemplateSnippets.ts | 3 +- .../browser/commands/insertSnippet.ts | 4 +- .../browser/commands/surroundWithSnippet.ts | 7 +- .../browser/snippetCodeActionProvider.ts | 2 +- .../browser/snippetCompletionProvider.ts | 2 +- .../contrib/snippets/browser/snippetPicker.ts | 5 +- .../contrib/snippets/browser/snippets.ts | 4 +- .../contrib/snippets/browser/snippetsFile.ts | 18 ++-- .../snippets/browser/snippetsService.ts | 18 ++-- .../contrib/snippets/browser/tabCompletion.ts | 2 +- .../snippets/test/browser/snippetFile.test.ts | 86 ------------------- .../test/browser/snippetsService.test.ts | 83 +++++++++++++++++- 12 files changed, 114 insertions(+), 120 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts index beea5ed367d86..fa5f708780322 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/fileTemplateSnippets.ts @@ -39,7 +39,8 @@ export class ApplyFileSnippetAction extends SnippetsAction { return; } - const snippets = await snippetService.getSnippets(undefined, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); + const resourceUri = editor.getModel().uri; + const snippets = await snippetService.getSnippets(undefined, resourceUri, { fileTemplateSnippets: true, noRecencySort: true, includeNoPrefixSnippets: true }); if (snippets.length === 0) { return; } diff --git a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts index 5d71cf5e8cf28..338e0ff196a13 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/insertSnippet.ts @@ -127,13 +127,13 @@ export class InsertSnippetAction extends SnippetEditorAction { if (name) { // take selected snippet - snippetService.getSnippets(languageId, { includeNoPrefixSnippets: true }) + snippetService.getSnippets(languageId, undefined, { includeNoPrefixSnippets: true }) .then(snippets => snippets.find(snippet => snippet.name === name)) .then(resolve, reject); } else { // let user pick a snippet - resolve(instaService.invokeFunction(pickSnippet, languageId)); + resolve(instaService.invokeFunction(pickSnippet, languageId, editor.getModel().uri)); } }); diff --git a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts index e9f9a63d98ab0..393e0c2bab1aa 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/surroundWithSnippet.ts @@ -23,7 +23,7 @@ export async function getSurroundableSnippets(snippetsService: ISnippetsService, model.tokenization.tokenizeIfCheap(lineNumber); const languageId = model.getLanguageIdAtPosition(lineNumber, column); - const allSnippets = await snippetsService.getSnippets(languageId, { includeNoPrefixSnippets: true, includeDisabledSnippets }); + const allSnippets = await snippetsService.getSnippets(languageId, model.uri, { includeNoPrefixSnippets: true, includeDisabledSnippets }); return allSnippets.filter(snippet => snippet.usesSelection); } @@ -54,12 +54,13 @@ export class SurroundWithSnippetEditorAction extends SnippetEditorAction { const snippetsService = accessor.get(ISnippetsService); const clipboardService = accessor.get(IClipboardService); - const snippets = await getSurroundableSnippets(snippetsService, editor.getModel(), editor.getPosition(), true); + const model = editor.getModel(); + const snippets = await getSurroundableSnippets(snippetsService, model, editor.getPosition(), true); if (!snippets.length) { return; } - const snippet = await instaService.invokeFunction(pickSnippet, snippets); + const snippet = await instaService.invokeFunction(pickSnippet, snippets, model.uri); if (!snippet) { return; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts index 53802851eca5b..ede209f332c29 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCodeActionProvider.ts @@ -88,7 +88,7 @@ class FileTemplateCodeActionProvider implements CodeActionProvider { return undefined; } - const snippets = await this._snippetService.getSnippets(model.getLanguageId(), { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); + const snippets = await this._snippetService.getSnippets(model.getLanguageId(), model.uri, { fileTemplateSnippets: true, includeNoPrefixSnippets: true }); const actions: CodeAction[] = []; for (const snippet of snippets) { if (actions.length >= FileTemplateCodeActionProvider._MAX_CODE_ACTIONS) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 8e4f1d375b5db..abc6ff72545b1 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -105,7 +105,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const triggerCharacterLow = context.triggerCharacter?.toLowerCase() ?? ''; const languageId = this._getLanguageIdAtPosition(model, position); const languageConfig = this._languageConfigurationService.getLanguageConfiguration(languageId); - const snippets = new Set(await this._snippets.getSnippets(languageId, undefined, model.uri)); + const snippets = new Set(await this._snippets.getSnippets(languageId, model.uri)); const suggestions: SnippetCompletion[] = []; for (const snippet of snippets) { diff --git a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts index 205fd707021d1..99839fac08a1a 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetPicker.ts @@ -12,8 +12,9 @@ import { ThemeIcon } from '../../../../base/common/themables.js'; import { Event } from '../../../../base/common/event.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; -export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippets: string | Snippet[]): Promise { +export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippets: string | Snippet[], resourceUri?: URI): Promise { const snippetService = accessor.get(ISnippetsService); const quickInputService = accessor.get(IQuickInputService); @@ -26,7 +27,7 @@ export async function pickSnippet(accessor: ServicesAccessor, languageIdOrSnippe if (Array.isArray(languageIdOrSnippets)) { snippets = languageIdOrSnippets; } else { - snippets = (await snippetService.getSnippets(languageIdOrSnippets, { includeDisabledSnippets: true, includeNoPrefixSnippets: true })); + snippets = (await snippetService.getSnippets(languageIdOrSnippets, resourceUri, { includeDisabledSnippets: true, includeNoPrefixSnippets: true })); } snippets.sort((a, b) => a.snippetSource - b.snippetSource); diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.ts b/src/vs/workbench/contrib/snippets/browser/snippets.ts index 67a65629e075c..6a7dfdaa5bcc4 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.ts @@ -28,7 +28,7 @@ export interface ISnippetsService { updateUsageTimestamp(snippet: Snippet): void; - getSnippets(languageId: string | undefined, opt?: ISnippetGetOptions, resourceUri?: URI): Promise; + getSnippets(languageId: string | undefined, resourceUri?: URI, opt?: ISnippetGetOptions): Promise; - getSnippetsSync(languageId: string, opt?: ISnippetGetOptions, resourceUri?: URI): Snippet[]; + getSnippetsSync(languageId: string, resourceUri?: URI, opt?: ISnippetGetOptions): Snippet[]; } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 2a88df7acc293..0768789b0ca0b 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -150,7 +150,7 @@ export class Snippet { if (this.exclude) { if (!this._parsedExclude) { const expression: IExpression = {}; - for (const pattern of this.exclude.filter(p => Boolean(p))) { + for (const pattern of this.exclude.filter(Boolean)) { expression[pattern] = true; } this._parsedExclude = parseGlob(expression, { ignoreCase: true }); @@ -163,7 +163,7 @@ export class Snippet { if (this.include) { if (!this._parsedInclude) { const expression: IExpression = {}; - for (const pattern of this.include.filter(p => Boolean(p))) { + for (const pattern of this.include.filter(Boolean)) { expression[pattern] = true; } this._parsedInclude = parseGlob(expression, { ignoreCase: true }); @@ -220,28 +220,24 @@ export class SnippetFile { this.isUserSnippets = !this._extension; } - select(selector: string, bucket: Snippet[], resourceUri?: URI): void { + select(selector: string, bucket: Snippet[]): void { if (this.isGlobalSnippets || !this.isUserSnippets) { - this._scopeSelect(selector, bucket, resourceUri); + this._scopeSelect(selector, bucket); } else { this._filepathSelect(selector, bucket); } } private _filepathSelect(selector: string, bucket: Snippet[]): void { - // for `fooLang.json` files all snippets are accepted + // for `fooLang.json` files apply inclusion/exclusion rules only if (selector + '.json' === basename(this.location.path)) { bucket.push(...this.data); } } - private _scopeSelect(selector: string, bucket: Snippet[], resourceUri?: URI): void { + private _scopeSelect(selector: string, bucket: Snippet[]): void { // for `my.code-snippets` files we need to look at each snippet for (const snippet of this.data) { - if (resourceUri && !snippet.isFileIncluded(resourceUri)) { - continue; - } - const len = snippet.scopes.length; if (len === 0) { // always accept @@ -260,7 +256,7 @@ export class SnippetFile { const idx = selector.lastIndexOf('.'); if (idx >= 0) { - this._scopeSelect(selector.substring(0, idx), bucket, resourceUri); + this._scopeSelect(selector.substring(0, idx), bucket); } } diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts index c13332bea4e59..4c681e481e62e 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsService.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsService.ts @@ -265,7 +265,7 @@ export class SnippetsService implements ISnippetsService { return this._files.values(); } - async getSnippets(languageId: string | undefined, opts?: ISnippetGetOptions, resourceUri?: URI): Promise { + async getSnippets(languageId: string | undefined, resourceUri?: URI, opts?: ISnippetGetOptions): Promise { await this._joinSnippets(); const result: Snippet[] = []; @@ -275,7 +275,7 @@ export class SnippetsService implements ISnippetsService { if (this._languageService.isRegisteredLanguageId(languageId)) { for (const file of this._files.values()) { promises.push(file.load() - .then(file => file.select(languageId, result, resourceUri)) + .then(file => file.select(languageId, result)) .catch(err => this._logService.error(err, file.location.toString())) ); } @@ -289,23 +289,23 @@ export class SnippetsService implements ISnippetsService { } } await Promise.all(promises); - return this._filterAndSortSnippets(result, opts); + return this._filterAndSortSnippets(result, resourceUri, opts); } - getSnippetsSync(languageId: string, opts?: ISnippetGetOptions, resourceUri?: URI): Snippet[] { + getSnippetsSync(languageId: string, resourceUri?: URI, opts?: ISnippetGetOptions): Snippet[] { const result: Snippet[] = []; if (this._languageService.isRegisteredLanguageId(languageId)) { for (const file of this._files.values()) { // kick off loading (which is a noop in case it's already loaded) // and optimistically collect snippets file.load().catch(_err => { /*ignore*/ }); - file.select(languageId, result, resourceUri); + file.select(languageId, result); } } - return this._filterAndSortSnippets(result, opts); + return this._filterAndSortSnippets(result, resourceUri, opts); } - private _filterAndSortSnippets(snippets: Snippet[], opts?: ISnippetGetOptions): Snippet[] { + private _filterAndSortSnippets(snippets: Snippet[], resourceUri?: URI, opts?: ISnippetGetOptions): Snippet[] { const result: Snippet[] = []; @@ -322,6 +322,10 @@ export class SnippetsService implements ISnippetsService { // isTopLevel requested but mismatching continue; } + if (resourceUri && !snippet.isFileIncluded(resourceUri)) { + // include/exclude settings don't match + continue; + } result.push(snippet); } diff --git a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts index 43dd710d128e9..1e6db4d54c18f 100644 --- a/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts +++ b/src/vs/workbench/contrib/snippets/browser/tabCompletion.ts @@ -93,7 +93,7 @@ export class TabCompletionController implements IEditorContribution { const model = this._editor.getModel(); model.tokenization.tokenizeIfCheap(selection.positionLineNumber); const id = model.getLanguageIdAtPosition(selection.positionLineNumber, selection.positionColumn); - const snippets = this._snippetService.getSnippetsSync(id); + const snippets = this._snippetService.getSnippetsSync(id, model.uri); if (!snippets) { // nothing for this language diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts index 2fb299cc7538f..8274299f3da00 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetFile.test.ts @@ -102,90 +102,4 @@ suite('Snippets', function () { assertIsTrivial('${1:foo}', false); }); - test('SnippetFile#select - include pattern', function () { - - const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ - new Snippet(false, ['typescript'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts']), - new Snippet(false, ['typescript'], 'SpecSnippet', 'spec', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.spec.ts']), - new Snippet(false, ['typescript'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), - ]); - - // Test file should only get TestSnippet and AllSnippet - let bucket: Snippet[] = []; - file.select('typescript', bucket, URI.file('/project/src/foo.test.ts')); - assert.strictEqual(bucket.length, 2); - assert.ok(bucket.some(s => s.name === 'TestSnippet')); - assert.ok(bucket.some(s => s.name === 'AllSnippet')); - - // Spec file should only get SpecSnippet and AllSnippet - bucket = []; - file.select('typescript', bucket, URI.file('/project/src/foo.spec.ts')); - assert.strictEqual(bucket.length, 2); - assert.ok(bucket.some(s => s.name === 'SpecSnippet')); - assert.ok(bucket.some(s => s.name === 'AllSnippet')); - - // Regular file should only get AllSnippet - bucket = []; - file.select('typescript', bucket, URI.file('/project/src/foo.ts')); - assert.strictEqual(bucket.length, 1); - assert.strictEqual(bucket[0].name, 'AllSnippet'); - - // Without URI, all snippets should be selected (backward compatibility) - bucket = []; - file.select('typescript', bucket); - assert.strictEqual(bucket.length, 3); - }); - - test('SnippetFile#select - exclude pattern', function () { - - const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ - new Snippet(false, ['javascript'], 'ProdSnippet', 'prod', '', 'snippet', 'test', SnippetSource.User, generateUuid(), undefined, ['**/*.min.js', '**/dist/**']), - new Snippet(false, ['javascript'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), - ]); - - // Regular .js file should get both snippets - let bucket: Snippet[] = []; - file.select('javascript', bucket, URI.file('/project/src/foo.js')); - assert.strictEqual(bucket.length, 2); - - // Minified file should only get AllSnippet (ProdSnippet is excluded) - bucket = []; - file.select('javascript', bucket, URI.file('/project/src/foo.min.js')); - assert.strictEqual(bucket.length, 1); - assert.strictEqual(bucket[0].name, 'AllSnippet'); - - // File in dist folder should only get AllSnippet - bucket = []; - file.select('javascript', bucket, URI.file('/project/dist/bundle.js')); - assert.strictEqual(bucket.length, 1); - assert.strictEqual(bucket[0].name, 'AllSnippet'); - }); - - test('SnippetFile#select - include and exclude patterns together', function () { - - const file = new TestSnippetFile(URI.file('somepath/foo.code-snippets'), [ - new Snippet(false, ['typescript'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts', '**/*.spec.ts'], ['**/*.perf.test.ts']), - ]); - - // Regular test file should get the snippet - let bucket: Snippet[] = []; - file.select('typescript', bucket, URI.file('/project/src/foo.test.ts')); - assert.strictEqual(bucket.length, 1); - - // Spec file should get the snippet - bucket = []; - file.select('typescript', bucket, URI.file('/project/src/foo.spec.ts')); - assert.strictEqual(bucket.length, 1); - - // Performance test file should NOT get the snippet (excluded) - bucket = []; - file.select('typescript', bucket, URI.file('/project/src/foo.perf.test.ts')); - assert.strictEqual(bucket.length, 0); - - // Regular file should NOT get the snippet (not included) - bucket = []; - file.select('typescript', bucket, URI.file('/project/src/foo.ts')); - assert.strictEqual(bucket.length, 0); - }); - }); diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 6dd26435753b5..5024a7d9b8902 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -22,14 +22,19 @@ import { CompletionModel } from '../../../../../editor/contrib/suggest/browser/c import { CompletionItem } from '../../../../../editor/contrib/suggest/browser/suggest.js'; import { WordDistance } from '../../../../../editor/contrib/suggest/browser/wordDistance.js'; import { EditorOptions } from '../../../../../editor/common/config/editorOptions.js'; +import { URI } from '../../../../../base/common/uri.js'; class SimpleSnippetService implements ISnippetsService { declare readonly _serviceBrand: undefined; constructor(readonly snippets: Snippet[]) { } - getSnippets() { - return Promise.resolve(this.getSnippetsSync()); + getSnippets(languageId?: string, resourceUri?: URI) { + return Promise.resolve(this.getSnippetsSync(languageId!, resourceUri)); } - getSnippetsSync(): Snippet[] { + getSnippetsSync(languageId?: string, resourceUri?: URI): Snippet[] { + // Filter snippets based on resourceUri if provided + if (resourceUri) { + return this.snippets.filter(snippet => snippet.isFileIncluded(resourceUri)); + } return this.snippets; } getSnippetFiles(): any { @@ -1057,4 +1062,76 @@ suite('SnippetsService', function () { assert.strictEqual(result2.suggestions.length, 1); } }); + + test('getSnippetsSync - include pattern', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts']), + new Snippet(false, ['fooLang'], 'SpecSnippet', 'spec', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.spec.ts']), + new Snippet(false, ['fooLang'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Test file should only get TestSnippet and AllSnippet + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 2); + assert.ok(snippets.some(s => s.name === 'TestSnippet')); + assert.ok(snippets.some(s => s.name === 'AllSnippet')); + + // Spec file should only get SpecSnippet and AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(snippets.length, 2); + assert.ok(snippets.some(s => s.name === 'SpecSnippet')); + assert.ok(snippets.some(s => s.name === 'AllSnippet')); + + // Regular file should only get AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + + // Without URI, all snippets should be returned (backward compatibility) + snippets = snippetService.getSnippetsSync('fooLang'); + assert.strictEqual(snippets.length, 3); + }); + + test('getSnippetsSync - exclude pattern', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'ProdSnippet', 'prod', '', 'snippet', 'test', SnippetSource.User, generateUuid(), undefined, ['**/*.min.js', '**/dist/**']), + new Snippet(false, ['fooLang'], 'AllSnippet', 'all', '', 'snippet', 'test', SnippetSource.User, generateUuid()), + ]); + + // Regular .js file should get both snippets + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.js')); + assert.strictEqual(snippets.length, 2); + + // Minified file should only get AllSnippet (ProdSnippet is excluded) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.min.js')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + + // File in dist folder should only get AllSnippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/dist/bundle.js')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'AllSnippet'); + }); + + test('getSnippetsSync - include and exclude patterns together', function () { + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['**/*.test.ts', '**/*.spec.ts'], ['**/*.perf.test.ts']), + ]); + + // Regular test file should get the snippet + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 1); + + // Spec file should get the snippet + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.spec.ts')); + assert.strictEqual(snippets.length, 1); + + // Performance test file should NOT get the snippet (excluded) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.perf.test.ts')); + assert.strictEqual(snippets.length, 0); + + // Regular file should NOT get the snippet (not included) + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.ts')); + assert.strictEqual(snippets.length, 0); + }); }); From 3affca704e652924f5dcab3d74f8d4519ec63341 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 9 Jan 2026 11:01:15 +0100 Subject: [PATCH 3/4] Enable relative filename matching when / is not in the pattern --- .../contrib/snippets/browser/snippetsFile.ts | 30 ++++++++----------- .../test/browser/snippetsService.test.ts | 30 +++++++++++++++++++ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 0768789b0ca0b..269e79842a984 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -16,7 +16,7 @@ import { relativePath } from '../../../../base/common/resources.js'; import { isObject } from '../../../../base/common/types.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { WindowIdleValue, getActiveWindow } from '../../../../base/browser/dom.js'; -import { parse as parseGlob, ParsedExpression, IExpression } from '../../../../base/common/glob.js'; +import { match as matchGlob } from '../../../../base/common/glob.js'; class SnippetBodyInsights { @@ -101,8 +101,6 @@ class SnippetBodyInsights { export class Snippet { private readonly _bodyInsights: WindowIdleValue; - private _parsedInclude?: ParsedExpression; - private _parsedExclude?: ParsedExpression; readonly prefixLow: string; @@ -146,29 +144,27 @@ export class Snippet { isFileIncluded(resourceUri: URI): boolean { const filePath = resourceUri.fsPath; + const fileName = basename(filePath); + + const getMatchTarget = (pattern: string): string => { + return pattern.includes('/') ? filePath : fileName; + }; if (this.exclude) { - if (!this._parsedExclude) { - const expression: IExpression = {}; - for (const pattern of this.exclude.filter(Boolean)) { - expression[pattern] = true; + for (const pattern of this.exclude.filter(Boolean)) { + if (matchGlob(pattern, getMatchTarget(pattern), { ignoreCase: true })) { + return false; } - this._parsedExclude = parseGlob(expression, { ignoreCase: true }); - } - if (this._parsedExclude(filePath)) { - return false; } } if (this.include) { - if (!this._parsedInclude) { - const expression: IExpression = {}; - for (const pattern of this.include.filter(Boolean)) { - expression[pattern] = true; + for (const pattern of this.include.filter(Boolean)) { + if (matchGlob(pattern, getMatchTarget(pattern), { ignoreCase: true })) { + return true; } - this._parsedInclude = parseGlob(expression, { ignoreCase: true }); } - return !!this._parsedInclude(filePath); + return false; } return true; diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts index 5024a7d9b8902..14afd71b76a94 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -1134,4 +1134,34 @@ suite('SnippetsService', function () { snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.ts')); assert.strictEqual(snippets.length, 0); }); + + test('getSnippetsSync - filename-only patterns (no path separator)', function () { + // Patterns without '/' should match on filename only (like files.associations) + snippetService = new SimpleSnippetService([ + new Snippet(false, ['fooLang'], 'TestSnippet', 'test', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['*.test.ts']), + new Snippet(false, ['fooLang'], 'ConfigSnippet', 'config', '', 'snippet', 'test', SnippetSource.User, generateUuid(), ['config.json']), + ]); + + // *.test.ts should match any file ending in .test.ts regardless of path + let snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/src/foo.test.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'TestSnippet'); + + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/other/deep/path/bar.test.ts')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'TestSnippet'); + + // config.json should match filename exactly + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/config.json')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'ConfigSnippet'); + + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/deep/nested/path/config.json')); + assert.strictEqual(snippets.length, 1); + assert.strictEqual(snippets[0].name, 'ConfigSnippet'); + + // myconfig.json should NOT match config.json pattern + snippets = snippetService.getSnippetsSync('fooLang', URI.file('/project/myconfig.json')); + assert.strictEqual(snippets.length, 0); + }); }); From 0b46c6974fc720814769f60d8bb71f4935e2b2f8 Mon Sep 17 00:00:00 2001 From: Dmitriy Vasyura Date: Fri, 9 Jan 2026 11:23:44 +0100 Subject: [PATCH 4/4] Update examples, use proper file path from URI. --- .../snippets/browser/commands/configureSnippets.ts | 8 ++++---- .../contrib/snippets/browser/snippets.contribution.ts | 4 ++-- src/vs/workbench/contrib/snippets/browser/snippetsFile.ts | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts index c6075dfeae732..6add03050d132 100644 --- a/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts +++ b/src/vs/workbench/contrib/snippets/browser/commands/configureSnippets.ts @@ -198,8 +198,8 @@ async function createSnippetFile(scope: string, defaultPath: URI, quickInputServ '\t// \t"scope": "javascript,typescript",', '\t// \t"prefix": "test",', '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', - '\t// \t"include": ["**/*.test.ts", "**/*.spec.ts"],', - '\t// \t"exclude": ["**/*.min.js"],', + '\t// \t"include": ["**/*.test.ts", "*.spec.ts"],', + '\t// \t"exclude": ["**/temp/*.ts"],', '\t// \t"description": "Insert test block"', '\t// }', '}' @@ -233,8 +233,8 @@ async function createLanguageSnippetFile(pick: ISnippetPick, fileService: IFileS '\t// "Test snippet": {', '\t// \t"prefix": "test",', '\t// \t"body": "test(\'$1\', () => {\\n\\t$0\\n});",', - '\t// \t"include": ["**/*.test.ts", "**/*.spec.ts"],', - '\t// \t"exclude": ["**/*.min.js"],', + '\t// \t"include": ["**/*.test.ts", "*.spec.ts"],', + '\t// \t"exclude": ["**/temp/*.ts"],', '\t// \t"description": "Insert test block"', '\t// }', '}' diff --git a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts index 0266acd11d707..46ccc93bb5969 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippets.contribution.ts @@ -77,14 +77,14 @@ const snippetSchemaProperties: IJSONSchemaMap = { type: ['string', 'array'] }, include: { - markdownDescription: nls.localize('snippetSchema.json.include', 'A list of glob patterns to include the snippet for specific files, e.g. `["**/*.test.ts", "**/*.spec.ts"]` or `"**/travis.yml"`.'), + markdownDescription: nls.localize('snippetSchema.json.include', 'A list of glob patterns to include the snippet for specific files, e.g. `["**/*.test.ts", "*.spec.ts"]` or `"**/*.spec.ts"`.'), type: ['string', 'array'], items: { type: 'string' } }, exclude: { - markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of glob patterns to exclude the snippet from specific files, e.g. `["**/*.min.js"]`.'), + markdownDescription: nls.localize('snippetSchema.json.exclude', 'A list of glob patterns to exclude the snippet from specific files, e.g. `["**/*.min.js"]` or `"*.min.js"`.'), type: ['string', 'array'], items: { type: 'string' diff --git a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts index 269e79842a984..7d843f8f3db23 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetsFile.ts @@ -17,6 +17,7 @@ import { isObject } from '../../../../base/common/types.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { WindowIdleValue, getActiveWindow } from '../../../../base/browser/dom.js'; import { match as matchGlob } from '../../../../base/common/glob.js'; +import { Schemas } from '../../../../base/common/network.js'; class SnippetBodyInsights { @@ -143,11 +144,11 @@ export class Snippet { } isFileIncluded(resourceUri: URI): boolean { - const filePath = resourceUri.fsPath; - const fileName = basename(filePath); + const uriPath = resourceUri.scheme === Schemas.file ? resourceUri.fsPath : resourceUri.path; + const fileName = basename(uriPath); const getMatchTarget = (pattern: string): string => { - return pattern.includes('/') ? filePath : fileName; + return pattern.includes('/') ? uriPath : fileName; }; if (this.exclude) {