diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts index 4d032b5b3de97..464f47f66d56c 100644 --- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts +++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts @@ -17,6 +17,7 @@ import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/sn import { isPatternInWord } from 'vs/base/common/filters'; import { StopWatch } from 'vs/base/common/stopwatch'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; +import { getWordAtText } from 'vs/editor/common/model/wordHelper'; export class SnippetCompletion implements CompletionItem { @@ -70,6 +71,7 @@ export class SnippetCompletionProvider implements CompletionItemProvider { const snippets = new Set(await this._snippets.getSnippets(languageId)); const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase(); + const wordUntil = model.getWordUntilPosition(position).word.toLowerCase(); const suggestions: SnippetCompletion[] = []; const columnOffset = position.column - 1; @@ -78,6 +80,13 @@ export class SnippetCompletionProvider implements CompletionItemProvider { for (const snippet of snippets) { + const snippetPrefixWord = getWordAtText(1, LanguageConfigurationRegistry.getWordDefinition(languageId), snippet.prefixLow, 0); + + if (wordUntil && snippetPrefixWord && !isPatternInWord(wordUntil, 0, wordUntil.length, snippet.prefixLow, 0, snippet.prefixLow.length)) { + // when at a word the snippet prefix must match + continue; + } + for (let pos = Math.max(0, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) { if (context.triggerKind === CompletionTriggerKind.TriggerCharacter && !snippet.prefixLow.startsWith(triggerCharacterLow)) { 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 739ea1646e787..f5e77e30fc8e5 100644 --- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts +++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; +import { SnippetCompletion, SnippetCompletionProvider } from 'vs/workbench/contrib/snippets/browser/snippetCompletionProvider'; import { Position } from 'vs/editor/common/core/position'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { ModeServiceImpl } from 'vs/editor/common/services/modeServiceImpl'; @@ -91,12 +91,17 @@ suite('SnippetsService', function () { }); }); - test('snippet completions - simple 2', function () { + test('snippet completions - simple 2', async function () { const provider = new SnippetCompletionProvider(modeService, snippetService); const model = disposables.add(createTextModel('hello ', undefined, 'fooLang')); - return provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => { + await provider.provideCompletionItems(model, new Position(1, 6) /* hello| */, context)!.then(result => { + assert.strictEqual(result.incomplete, undefined); + assert.strictEqual(result.suggestions.length, 0); + }); + + await provider.provideCompletionItems(model, new Position(1, 7) /* hello |*/, context)!.then(result => { assert.strictEqual(result.incomplete, undefined); assert.strictEqual(result.suggestions.length, 2); }); @@ -640,4 +645,67 @@ suite('SnippetsService', function () { assert.strictEqual(result.suggestions.length, 1); model.dispose(); }); + + test('Snippet suggestions are too eager #138707 (word)', async function () { + snippetService = new SimpleSnippetService([ + new Snippet(['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User), + new Snippet(['fooLang'], 'hell_or_tell', 'hell_or_tell', '', 'value', '', SnippetSource.User), + new Snippet(['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User), + ]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + let model = createTextModel('\'hellot\'', undefined, 'fooLang'); + + let result = await provider.provideCompletionItems( + model, + new Position(1, 8), + { triggerKind: CompletionTriggerKind.Invoke } + )!; + + assert.strictEqual(result.suggestions.length, 2); + assert.strictEqual((result.suggestions[0]).label.label, '^y'); + assert.strictEqual((result.suggestions[1]).label.label, 'hell_or_tell'); + model.dispose(); + }); + + test('Snippet suggestions are too eager #138707 (no word)', async function () { + snippetService = new SimpleSnippetService([ + new Snippet(['fooLang'], 'tys', 'tys', '', 'value', '', SnippetSource.User), + new Snippet(['fooLang'], 't', 't', '', 'value', '', SnippetSource.User), + new Snippet(['fooLang'], '^y', '^y', '', 'value', '', SnippetSource.User), + ]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + let model = createTextModel(')*&^', undefined, 'fooLang'); + + let result = await provider.provideCompletionItems( + model, + new Position(1, 5), + { triggerKind: CompletionTriggerKind.Invoke } + )!; + + assert.strictEqual(result.suggestions.length, 1); + assert.strictEqual((result.suggestions[0]).label.label, '^y'); + model.dispose(); + }); + + test('Snippet suggestions are too eager #138707 (word/word)', async function () { + snippetService = new SimpleSnippetService([ + new Snippet(['fooLang'], 'async arrow function', 'async arrow function', '', 'value', '', SnippetSource.User), + new Snippet(['fooLang'], 'foobarrrrrr', 'foobarrrrrr', '', 'value', '', SnippetSource.User), + ]); + + const provider = new SnippetCompletionProvider(modeService, snippetService); + let model = createTextModel('foobar', undefined, 'fooLang'); + + let result = await provider.provideCompletionItems( + model, + new Position(1, 7), + { triggerKind: CompletionTriggerKind.Invoke } + )!; + + assert.strictEqual(result.suggestions.length, 1); + assert.strictEqual((result.suggestions[0]).label.label, 'foobarrrrrr'); + model.dispose(); + }); });