Repository: microsoft/vscode (TypeScript)
Date: 2021-01-12 23:58:19+00:00
Task type: Bug Fix

Issue:
Snippets + Auto-closing pairs don't compliment each other
<!-- ⚠️⚠️ Do Not Delete This! bug_report_template ⚠️⚠️ -->
<!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ -->
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Also please test using the latest insiders build to make sure your issue has not already been fixed: https://code.visualstudio.com/insiders/ -->

<!-- Use Help > Report Issue to prefill these. -->
Version: 1.49.0-insider
Commit: 11dc5a81ba248cc2678888391f1b24dccabddaf8
Date: 2020-08-17T17:11:16.078Z (1 day ago)
Electron: 9.2.0
Chrome: 83.0.4103.122
Node.js: 12.14.1
V8: 8.3.110.13-electron.0
OS: Darwin x64 19.6.0

Steps to Reproduce:

Add the following python snippet:
```json
"listcompPy": {
		"prefix":"[x for x in",
		"body":[
			"[x for x in range(1,20) if x%2==0 ]"
		]
	}
```
or PowerShell snippet:
```json
"PSCustomObject": {
        "prefix":"[PSCustomObject",
        "body":[
            "[PSCustomObject] @{ Key = Value }"
        ]
    }
```

1. type: `[` which will cause auto-closing pair... (at this point you have `[|]` where `|` is the cursor)
2. then add a `x` or `psc` and hit TAB or ENTER on the intellisense item

### Expected

#### Python

```
[x for x in range(1,20) if x%2==0 ]
```

#### PowerShell

```
[PSCustomObject]@{
    Name = Value
}
```

### Actual

#### Python

```
[x for x in range(1,20) if x%2==0 ]]
```

#### PowerShell

```
[PSCustomObject]@{
    Name = Value
}]
```

NOTE THE TRAILING `]`. Snippets and auto-closing pairs should play nice together so that the whole experience generates valid code.

<!-- Launch with `code --disable-extensions` to check. -->
Does this issue occur when all extensions are disabled?: Yes



Issue discussion:
Here's a javascript example for array destructuring:

```json
"arraydestructuring": {
        "prefix": "[a",
        "body": [
            "[a, b, ...next] = ${1:array}"
        ],
        "description": "arraydestructuring"
    }
```

which gets you:

```
[a, b, ...next] = array]
```

when you type: `[a<TAB or ENTER to accept intellisense>`
better example!

A javascript example for `Symbol.iterator`:

```json
"SymbolIterator": {
		"prefix": "[Symbol.iterator",
		"body": [
			"[Symbol.iterator]: () => {",
			"    return {",
			"        next: () => {",
			"            return { value: value, done: false };",
			"        }",
			"    };",
			"}"
		]
	}
```

which gets you:

```js
[Symbol.iterator]: () => {
        return {
            next: () => {
                return { value: value, done: false };
            }
        };
    }]
```
> Notice the extra `]` at the end

when you type: `[Symbol<TAB or ENTER to accept intellisense>`



Solution patch:
diff --git a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts
--- a/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts
+++ b/src/vs/workbench/contrib/snippets/browser/snippetCompletionProvider.ts
@@ -16,6 +16,7 @@ import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets
 import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
 import { isPatternInWord } from 'vs/base/common/filters';
 import { StopWatch } from 'vs/base/common/stopwatch';
+import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
 
 export class SnippetCompletion implements CompletionItem {
 
@@ -110,8 +111,27 @@ export class SnippetCompletionProvider implements CompletionItemProvider {
 					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 endColumn = endsWithPrefixRest === 0 ? position.column + prefixRestLen : position.column;
-					const replace = Range.fromPositions(position.delta(0, -prefixPos), { lineNumber: position.lineNumber, column: endColumn });
+					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++;
+						}
+					}
+
+					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 }));


Test patch:
diff --git a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts
--- a/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts
+++ b/src/vs/workbench/contrib/snippets/test/browser/snippetsService.test.ts
@@ -13,6 +13,7 @@ import { ISnippetsService } from 'vs/workbench/contrib/snippets/browser/snippets
 import { Snippet, SnippetSource } from 'vs/workbench/contrib/snippets/browser/snippetsFile';
 import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
 import { CompletionContext, CompletionTriggerKind } from 'vs/editor/common/modes';
+import { DisposableStore } from 'vs/base/common/lifecycle';
 
 class SimpleSnippetService implements ISnippetsService {
 	declare readonly _serviceBrand: undefined;
@@ -35,6 +36,8 @@ class SimpleSnippetService implements ISnippetsService {
 }
 
 suite('SnippetsService', function () {
+	const disposableStore: DisposableStore = new DisposableStore();
+	const context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke };
 
 	suiteSetup(function () {
 		ModesRegistry.registerLanguage({
@@ -43,9 +46,12 @@ suite('SnippetsService', function () {
 		});
 	});
 
+	suiteTeardown(function () {
+		disposableStore.dispose();
+	});
+
 	let modeService: ModeServiceImpl;
 	let snippetService: ISnippetsService;
-	let context: CompletionContext = { triggerKind: CompletionTriggerKind.Invoke };
 
 	setup(function () {
 		modeService = new ModeServiceImpl();
@@ -68,7 +74,6 @@ suite('SnippetsService', function () {
 		)]);
 	});
 
-
 	test('snippet completions - simple', function () {
 
 		const provider = new SnippetCompletionProvider(modeService, snippetService);
@@ -364,9 +369,10 @@ suite('SnippetsService', function () {
 	});
 
 	test('issue #61296: VS code freezes when editing CSS file with emoji', async function () {
-		let toDispose = LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, {
+		disposableStore.add(LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, {
 			wordPattern: /(#?-?\d*\.\d\w*%?)|(::?[\w-]*(?=[^,{;]*[,{]))|(([@#.!])?[\w-?]+%?|[@#!.])/g
-		});
+		}));
+
 		snippetService = new SimpleSnippetService([new Snippet(
 			['fooLang'],
 			'bug',
@@ -383,8 +389,6 @@ suite('SnippetsService', function () {
 		let result = await provider.provideCompletionItems(model, new Position(1, 8), context)!;
 
 		assert.equal(result.suggestions.length, 1);
-
-		toDispose.dispose();
 	});
 
 	test('No snippets shown when triggering completions at whitespace on line that already has text #62335', async function () {
@@ -502,4 +506,35 @@ suite('SnippetsService', function () {
 		assert.equal((first.range as any).insert.endColumn, 9);
 		assert.equal((first.range as any).replace.endColumn, 9);
 	});
+
+	test('Snippet will replace auto-closing pair if specified in prefix', async function () {
+		disposableStore.add(LanguageConfigurationRegistry.register(modeService.getLanguageIdentifier('fooLang')!, {
+			brackets: [
+				['{', '}'],
+				['[', ']'],
+				['(', ')'],
+			]
+		}));
+
+		snippetService = new SimpleSnippetService([new Snippet(
+			['fooLang'],
+			'PSCustomObject',
+			'[PSCustomObject]',
+			'',
+			'[PSCustomObject] @{ Key = Value }',
+			'',
+			SnippetSource.User
+		)]);
+
+		const provider = new SnippetCompletionProvider(modeService, snippetService);
+
+		let model = createTextModel('[psc]', undefined, modeService.getLanguageIdentifier('fooLang'));
+		let result = await provider.provideCompletionItems(model, new Position(1, 5), context)!;
+
+		assert.strictEqual(result.suggestions.length, 1);
+		let [first] = result.suggestions;
+		assert.strictEqual((first.range as any).insert.endColumn, 5);
+		// This is 6 because it should eat the `]` at the end of the text even if cursor is before it
+		assert.strictEqual((first.range as any).replace.endColumn, 6);
+	});
 });