Change:
Issue Type: <bBug</bI would like the prefix to trigger only when there is a leading whitespace in front of the prefix (it is supposed to be substituted with an explicit non-breaking space character ). The example of the snippet: --The prefix behaves as if there is no leading space, meaning the remains one extra whitespace before the expanded body, resulting in a double space: . VS Code version: Code 1.55.2 (, 2021-04-13T09:35:57.887Z) OS version: WindowsNT x64 10.0. <details<summarySystem Info</summary </details<details<summaryExtensions (37)</summaryExtensionVersion ------ html-snippets0.2.1 Handlebars0.4.1 rc-beautify1.2.0 git-temporal-vscode1.0.0 insert-unicode0.10.0 i30.0.1 vscode-generate-getter-setter0.5.0 gitlens11.4.1 vscode-html-css1.10.2 vscode-npm-script0.3.21 carbon-now-sh1.2.0 vscode-angular-html1.7.0 beautify1.5.0 latex-workshop8.18.0 polacode-.5.2 svg1.4.6 Angular211.0.0 solidity0.0.113 Kotlin1.7.1 rainbow-csv1.8.1 vscode-docker1.12.1 python2021.5. vscode-pylance2021.5.2 jupyter2021.5. remote-containers0.166.1 postscript1.0.1 java0.79.1 LiveServer5.6.1 bash-debug0.3.9 scala0.5.3 bash-beautify0.1.1 code-settings-sync3.4.3 code-spell-checker1.10.2 sass-indented1.8.16 html-preview-vscode0.2.5 vscode-ruby0.28.0 markdown-all-in-one3.4.0 (2 theme extensions excluded) </details<details<summaryA/B Experiments</summary</details<!-- generated by issue reporter --
I have a fix for this but it makes bigger changes in how snippet completion are computed. Pushing out to November to get more Insider usage for it, changes are in

Discussion:
InsertAsSnippet = 0b100, } <ins> export interface CompletionItemRanges { insert: IRange; replace: IRange; } </ins> /** * A completion item represents a text snippet that is * proposed to complete text that is being typed.

* *Note:* The range must be a {@link Range.isSingleLine single line} and it must * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ <del> range: IRange | { insert: IRange, replace: IRange }; </del> <ins> range: IRange | CompletionItemRanges; </ins> /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous

InsertAsSnippet = 4 } <ins> export interface CompletionItemRanges { insert: IRange; replace: IRange; } </ins> /** * A completion item represents a text snippet that is * proposed to complete text that is being typed.

* *Note:* The range must be a {@link Range.isSingleLine single line} and it must * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. */ <del> range: IRange | { insert: IRange; replace: IRange; }; </del> <ins> range: IRange | CompletionItemRanges; </ins> /** * An optional set of characters that when pressed while this completion is active will accept it first and * then type that character. *Note* that all commit characters should have `length=1` and that superfluous

const sw = new StopWatch(true); const languageId = this._getLanguageIdAtPosition(model, position); <del> const snippets = await this._snippets.getSnippets(languageId); let pos = { lineNumber: position.lineNumber, column: 1 }; let lineOffsets: number[] = []; const lineContent = model.getLineContent(position.lineNumber).toLowerCase(); const endsInWhitespace = /s/.test(lineContent[position.column - 2]); while (pos.column < position.column) { let word = model.getWordAtPosition(pos); if (word) { // at a word lineOffsets.push(word.startColumn - 1); pos.column = word.endColumn + 1; if (word.endColumn < position.column && !/s/.test(lineContent[word.endColumn - 1])) { lineOffsets.push(word.endColumn - 1); } } else if (!/s/.test(lineContent[pos.column - 1])) { // at a none-whitespace character lineOffsets.push(pos.column - 1); pos.column += 1; } else { // always advance! pos.column += 1; } } </del> <ins> const snippets = new Set(await this._snippets.getSnippets(languageId)); </ins> <del> const availableSnippets = new Set<Snippet>(snippets); const suggestions: SnippetCompletion[] = []; </del> <ins> const lineContentLow = model.getLineContent(position.lineNumber).toLowerCase(); const suggestions: SnippetCompletion[] = []; </ins> const columnOffset = position.column - 1; <del> for (const start of lineOffsets) { availableSnippets.forEach(snippet => { if (isPatternInWord(lineContent, start, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { const prefixPos = position.column - (1 + start); const prefixRestLen = snippet.prefixLow.length - prefixPos; const endsWithPrefixRest = compareSubstring(lineContent, snippet.prefixLow, columnOffset, (columnOffset) + prefixRestLen, prefixPos, prefixPos + prefixRestLen); const startPosition = position.delta(0, -prefixPos); let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; // First check if there is anything to the right of the cursor if (columnOffset < lineContent.length) { const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContent[columnOffset]); // If the character to the right of the cursor is a closing character of an autoclosing pair if (standardAutoClosingPairConditionals?.some(p => // and the start position is the opening character of an autoclosing pair p.open === lineContent[startPosition.column - 1] && // and the snippet prefix contains the opening and closing pair at its edges snippet.prefix.startsWith(p.open) && snippet.prefix[snippet.prefix.length - 1] === p.close)) { // Eat the character that was likely inserted because of auto-closing pairs endColumn++; } } </del> <ins> for (const snippet of snippets) { </ins> <del> const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); const insert = replace.setEndPosition(position.lineNumber, position.column); </del> <ins> for (let pos = Math.max(0, columnOffset - snippet.prefixLow.length); pos < lineContentLow.length; pos++) { if (!isPatternInWord(lineContentLow, pos, columnOffset, snippet.prefixLow, 0, snippet.prefixLow.length)) { continue; } </ins> <del> suggestions.push(new SnippetCompletion(snippet, { replace, insert })); availableSnippets.delete(snippet); </del> <ins> const prefixRestLen = snippet.prefixLow.length - (columnOffset - pos); const endsWithPrefixRest = compareSubstring(lineContentLow, snippet.prefixLow, columnOffset, columnOffset + prefixRestLen, columnOffset - pos); const startPosition = position.with(undefined, pos + 1); let endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column; // First check if there is anything to the right of the cursor if (columnOffset < lineContentLow.length) { const autoClosingPairs = LanguageConfigurationRegistry.getAutoClosingPairs(languageId); const standardAutoClosingPairConditionals = autoClosingPairs.autoClosingPairsCloseSingleChar.get(lineContentLow[columnOffset]); // If the character to the right of the cursor is a closing character of an autoclosing pair if (standardAutoClosingPairConditionals?.some(p => // and the start position is the opening character of an autoclosing pair p.open === lineContentLow[startPosition.column - 1] && // and the snippet prefix contains the opening and closing pair at its edges snippet.prefix.startsWith(p.open) && snippet.prefix[snippet.prefix.length - 1] === p.close) ) { // Eat the character that was likely inserted because of auto-closing pairs endColumn++; } </ins> } <del> }); </del> <ins> const replace = Range.fromPositions(startPosition, { lineNumber: position.lineNumber, column: endColumn }); const insert = replace.setEndPosition(position.lineNumber, position.column); suggestions.push(new SnippetCompletion(snippet, { replace, insert })); snippets.delete(snippet); break; } </ins> } <del> if (endsInWhitespace || lineOffsets.length === 0) { // add remaing snippets when the current prefix ends in whitespace or when no // interesting positions have been found availableSnippets.forEach(snippet => { </del> <ins> const endsInWhitespace = /s/.test(lineContentLow[position.column - 2]); if (endsInWhitespace || !lineContentLow /*empty line*/) { // add remaing snippets when the current prefix ends in whitespace or when line is empty for (let snippet of snippets) { </ins> const insert = Range.fromPositions(position); <del> const replace = lineContent.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; </del> <ins> const replace = lineContentLow.indexOf(snippet.prefixLow, columnOffset) === columnOffset ? insert.setEndPosition(position.lineNumber, position.column + snippet.prefixLow.length) : insert; </ins> suggestions.push(new SnippetCompletion(snippet, { replace, insert })); <del> }); </del> <ins> } </ins> }

import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets.contribution'; import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; <del> import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes'; </del> <ins> import { CompletionContext, CompletionItemLabel, CompletionItemRanges, CompletionTriggerKind } from 'vs/editor/common/modes'; </ins> import { DisposableStore } from 'vs/base/common/lifecycle'; class SimpleSnippetService implements ISnippetsService {

}); }); <ins> test('snippet completions - simple 2', 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 => { assert.strictEqual(result.incomplete, undefined); assert.strictEqual(result.suggestions.length, 2); }); }); </ins> test('snippet completions - with prefix', function () { const provider = new SnippetCompletionProvider(modeService, snippetService);

description: 'barTest' }); assert.strictEqual(result.suggestions[0].insertText, 's1'); <del> assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); </del> <ins> assert.strictEqual((result.suggestions[0].range as CompletionItemRanges).insert.startColumn, 1); </ins> assert.deepStrictEqual(result.suggestions[1].label, { label: 'bar-bar', description: 'name' }); assert.strictEqual(result.suggestions[1].insertText, 's2'); <del> assert.strictEqual((result.suggestions[1].range as any).insert.startColumn, 1); </del> <ins> assert.strictEqual((result.suggestions[1].range as CompletionItemRanges).insert.startColumn, 1); </ins> }); await provider.provideCompletionItems(model, new Position(1, 5), context)!.then(result => { assert.strictEqual(result.incomplete, undefined); <del> assert.strictEqual(result.suggestions.length, 1); assert.deepStrictEqual(result.suggestions[0].label, { </del> <ins> assert.strictEqual(result.suggestions.length, 2); const [first, second] = result.suggestions; assert.deepStrictEqual(first.label, { label: 'bar', description: 'barTest' }); assert.strictEqual(first.insertText, 's1'); assert.strictEqual((first.range as CompletionItemRanges).insert.startColumn, 5); assert.deepStrictEqual(second.label, { </ins> label: 'bar-bar', description: 'name' }); <del> assert.strictEqual(result.suggestions[0].insertText, 's2'); assert.strictEqual((result.suggestions[0].range as any).insert.startColumn, 1); </del> <ins> assert.strictEqual(second.insertText, 's2'); assert.strictEqual((second.range as CompletionItemRanges).insert.startColumn, 1); </ins> }); await provider.provideCompletionItems(model, new Position(1, 6), context)!.then(result => {

assert.strictEqual((first.range as any).replace.endColumn, 6); model.dispose(); }); <ins> test('Leading whitespace in snippet prefix #123860', async function () { snippetService = new SimpleSnippetService([new Snippet( ['fooLang'], 'cite-name', ' cite', '', '~cite{$CLIPBOARD}', '', SnippetSource.User )]); const provider = new SnippetCompletionProvider(modeService, snippetService); let model = createTextModel(' ci', undefined, 'fooLang'); let result = await provider.provideCompletionItems(model, new Position(1, 4), context)!; assert.strictEqual(result.suggestions.length, 1); let [first] = result.suggestions; assert.strictEqual((<CompletionItemLabel>first.label).label, ' cite'); assert.strictEqual((<CompletionItemRanges>first.range).insert.startColumn, 1); model.dispose(); }); </ins> });