Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetController2.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import assert from 'assert';5import { mock } from '../../../../../base/test/common/mock.js';6import { CoreEditingCommands } from '../../../../browser/coreCommands.js';7import { ICodeEditor } from '../../../../browser/editorBrowser.js';8import { Selection } from '../../../../common/core/selection.js';9import { Range } from '../../../../common/core/range.js';10import { Handler } from '../../../../common/editorCommon.js';11import { TextModel } from '../../../../common/model/textModel.js';12import { SnippetController2 } from '../../browser/snippetController2.js';13import { createTestCodeEditor, ITestCodeEditor } from '../../../../test/browser/testCodeEditor.js';14import { createTextModel } from '../../../../test/common/testTextModel.js';15import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';16import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';17import { InstantiationService } from '../../../../../platform/instantiation/common/instantiationService.js';18import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';19import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';20import { ILabelService } from '../../../../../platform/label/common/label.js';21import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';22import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';23import { EndOfLineSequence } from '../../../../common/model.js';24import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';2526suite('SnippetController2', function () {2728/** @deprecated */29function assertSelections(editor: ICodeEditor, ...s: Selection[]) {30for (const selection of editor.getSelections()!) {31const actual = s.shift()!;32assert.ok(selection.equalsSelection(actual), `actual=${selection.toString()} <> expected=${actual.toString()}`);33}34assert.strictEqual(s.length, 0);35}3637function assertContextKeys(service: MockContextKeyService, inSnippet: boolean, hasPrev: boolean, hasNext: boolean): void {38const state = getContextState(service);39assert.strictEqual(state.inSnippet, inSnippet, `inSnippetMode`);40assert.strictEqual(state.hasPrev, hasPrev, `HasPrevTabstop`);41assert.strictEqual(state.hasNext, hasNext, `HasNextTabstop`);42}4344function getContextState(service: MockContextKeyService = contextKeys) {45return {46inSnippet: SnippetController2.InSnippetMode.getValue(service),47hasPrev: SnippetController2.HasPrevTabstop.getValue(service),48hasNext: SnippetController2.HasNextTabstop.getValue(service),49};50}5152let ctrl: SnippetController2;53let editor: ITestCodeEditor;54let model: TextModel;55let contextKeys: MockContextKeyService;56let instaService: IInstantiationService;5758setup(function () {59contextKeys = new MockContextKeyService();60model = createTextModel('if\n $state\nfi');61const serviceCollection = new ServiceCollection(62[ILabelService, new class extends mock<ILabelService>() { }],63[IWorkspaceContextService, new class extends mock<IWorkspaceContextService>() {64override getWorkspace() {65return { id: 'foo', folders: [] };66}67}],68[ILogService, new NullLogService()],69[IContextKeyService, contextKeys],70);71instaService = new InstantiationService(serviceCollection);72editor = createTestCodeEditor(model, { serviceCollection });73editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5)]);74assert.strictEqual(model.getEOL(), '\n');75});7677teardown(function () {78model.dispose();79ctrl.dispose();80});8182ensureNoDisposablesAreLeakedInTestSuite();8384test('creation', () => {85ctrl = instaService.createInstance(SnippetController2, editor);86assertContextKeys(contextKeys, false, false, false);87});8889test('insert, insert -> abort', function () {90ctrl = instaService.createInstance(SnippetController2, editor);9192ctrl.insert('foo${1:bar}foo$0');93assertContextKeys(contextKeys, true, false, true);94assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));9596ctrl.cancel();97assertContextKeys(contextKeys, false, false, false);98assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));99});100101test('insert, insert -> tab, tab, done', function () {102ctrl = instaService.createInstance(SnippetController2, editor);103104ctrl.insert('${1:one}${2:two}$0');105assertContextKeys(contextKeys, true, false, true);106107ctrl.next();108assertContextKeys(contextKeys, true, true, true);109110ctrl.next();111assertContextKeys(contextKeys, false, false, false);112113editor.trigger('test', 'type', { text: '\t' });114assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), false);115assert.strictEqual(SnippetController2.HasNextTabstop.getValue(contextKeys), false);116assert.strictEqual(SnippetController2.HasPrevTabstop.getValue(contextKeys), false);117});118119test('insert, insert -> cursor moves out (left/right)', function () {120ctrl = instaService.createInstance(SnippetController2, editor);121122ctrl.insert('foo${1:bar}foo$0');123assertContextKeys(contextKeys, true, false, true);124assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));125126// bad selection change127editor.setSelections([new Selection(1, 12, 1, 12), new Selection(2, 16, 2, 16)]);128assertContextKeys(contextKeys, false, false, false);129});130131test('insert, insert -> cursor moves out (up/down)', function () {132ctrl = instaService.createInstance(SnippetController2, editor);133134ctrl.insert('foo${1:bar}foo$0');135assertContextKeys(contextKeys, true, false, true);136assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));137138// bad selection change139editor.setSelections([new Selection(2, 4, 2, 7), new Selection(3, 8, 3, 11)]);140assertContextKeys(contextKeys, false, false, false);141});142143test('insert, insert -> cursors collapse', function () {144ctrl = instaService.createInstance(SnippetController2, editor);145146ctrl.insert('foo${1:bar}foo$0');147assert.strictEqual(SnippetController2.InSnippetMode.getValue(contextKeys), true);148assertSelections(editor, new Selection(1, 4, 1, 7), new Selection(2, 8, 2, 11));149150// bad selection change151editor.setSelections([new Selection(1, 4, 1, 7)]);152assertContextKeys(contextKeys, false, false, false);153});154155test('insert, insert plain text -> no snippet mode', function () {156ctrl = instaService.createInstance(SnippetController2, editor);157158ctrl.insert('foobar');159assertContextKeys(contextKeys, false, false, false);160assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));161});162163test('insert, delete snippet text', function () {164ctrl = instaService.createInstance(SnippetController2, editor);165166ctrl.insert('${1:foobar}$0');167assertContextKeys(contextKeys, true, false, true);168assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));169170editor.trigger('test', 'cut', {});171assertContextKeys(contextKeys, true, false, true);172assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(2, 5, 2, 5));173174editor.trigger('test', 'type', { text: 'abc' });175assertContextKeys(contextKeys, true, false, true);176177ctrl.next();178assertContextKeys(contextKeys, false, false, false);179180editor.trigger('test', 'tab', {});181assertContextKeys(contextKeys, false, false, false);182183// editor.trigger('test', 'type', { text: 'abc' });184// assertContextKeys(contextKeys, false, false, false);185});186187test('insert, nested trivial snippet', function () {188ctrl = instaService.createInstance(SnippetController2, editor);189ctrl.insert('${1:foo}bar$0');190assertContextKeys(contextKeys, true, false, true);191assertSelections(editor, new Selection(1, 1, 1, 4), new Selection(2, 5, 2, 8));192193ctrl.insert('FOO$0');194assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8));195assertContextKeys(contextKeys, true, false, true);196197ctrl.next();198assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));199assertContextKeys(contextKeys, false, false, false);200});201202test('insert, nested snippet', function () {203ctrl = instaService.createInstance(SnippetController2, editor);204ctrl.insert('${1:foobar}$0');205assertContextKeys(contextKeys, true, false, true);206assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));207208ctrl.insert('far$1boo$0');209assertSelections(editor, new Selection(1, 4, 1, 4), new Selection(2, 8, 2, 8));210assertContextKeys(contextKeys, true, false, true);211212ctrl.next();213assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));214assertContextKeys(contextKeys, true, true, true);215216ctrl.next();217assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));218assertContextKeys(contextKeys, false, false, false);219});220221test('insert, nested plain text', function () {222ctrl = instaService.createInstance(SnippetController2, editor);223ctrl.insert('${1:foobar}$0');224assertContextKeys(contextKeys, true, false, true);225assertSelections(editor, new Selection(1, 1, 1, 7), new Selection(2, 5, 2, 11));226227ctrl.insert('farboo');228assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));229assertContextKeys(contextKeys, true, false, true);230231ctrl.next();232assertSelections(editor, new Selection(1, 7, 1, 7), new Selection(2, 11, 2, 11));233assertContextKeys(contextKeys, false, false, false);234});235236test('Nested snippets without final placeholder jumps to next outer placeholder, #27898', function () {237ctrl = instaService.createInstance(SnippetController2, editor);238239ctrl.insert('for(const ${1:element} of ${2:array}) {$0}');240assertContextKeys(contextKeys, true, false, true);241assertSelections(editor, new Selection(1, 11, 1, 18), new Selection(2, 15, 2, 22));242243ctrl.next();244assertContextKeys(contextKeys, true, true, true);245assertSelections(editor, new Selection(1, 22, 1, 27), new Selection(2, 26, 2, 31));246247ctrl.insert('document');248assertContextKeys(contextKeys, true, true, true);249assertSelections(editor, new Selection(1, 30, 1, 30), new Selection(2, 34, 2, 34));250251ctrl.next();252assertContextKeys(contextKeys, false, false, false);253});254255test('Inconsistent tab stop behaviour with recursive snippets and tab / shift tab, #27543', function () {256ctrl = instaService.createInstance(SnippetController2, editor);257ctrl.insert('1_calize(${1:nl}, \'${2:value}\')$0');258259assertContextKeys(contextKeys, true, false, true);260assertSelections(editor, new Selection(1, 10, 1, 12), new Selection(2, 14, 2, 16));261262ctrl.insert('2_calize(${1:nl}, \'${2:value}\')$0');263264assertSelections(editor, new Selection(1, 19, 1, 21), new Selection(2, 23, 2, 25));265266ctrl.next(); // inner `value`267assertSelections(editor, new Selection(1, 24, 1, 29), new Selection(2, 28, 2, 33));268269ctrl.next(); // inner `$0`270assertSelections(editor, new Selection(1, 31, 1, 31), new Selection(2, 35, 2, 35));271272ctrl.next(); // outer `value`273assertSelections(editor, new Selection(1, 34, 1, 39), new Selection(2, 38, 2, 43));274275ctrl.prev(); // inner `$0`276assertSelections(editor, new Selection(1, 31, 1, 31), new Selection(2, 35, 2, 35));277});278279test('Snippet tabstop selecting content of previously entered variable only works when separated by space, #23728', function () {280ctrl = instaService.createInstance(SnippetController2, editor);281282model.setValue('');283editor.setSelection(new Selection(1, 1, 1, 1));284285ctrl.insert('import ${2:${1:module}} from \'${1:module}\'$0');286287assertContextKeys(contextKeys, true, false, true);288assertSelections(editor, new Selection(1, 8, 1, 14), new Selection(1, 21, 1, 27));289290ctrl.insert('foo');291assertSelections(editor, new Selection(1, 11, 1, 11), new Selection(1, 21, 1, 21));292293ctrl.next(); // ${2:...}294assertSelections(editor, new Selection(1, 8, 1, 11));295});296297test('HTML Snippets Combine, #32211', function () {298ctrl = instaService.createInstance(SnippetController2, editor);299300model.setValue('');301model.updateOptions({ insertSpaces: false, tabSize: 4, trimAutoWhitespace: false });302editor.setSelection(new Selection(1, 1, 1, 1));303304ctrl.insert(`305<!DOCTYPE html>306<html lang="en">307<head>308<meta charset="UTF-8">309<meta name="viewport" content="width=\${2:device-width}, initial-scale=\${3:1.0}">310<meta http-equiv="X-UA-Compatible" content="\${5:ie=edge}">311<title>\${7:Document}</title>312</head>313<body>314\${8}315</body>316</html>317`);318ctrl.next();319ctrl.next();320ctrl.next();321ctrl.next();322assertSelections(editor, new Selection(11, 5, 11, 5));323324ctrl.insert('<input type="${2:text}">');325assertSelections(editor, new Selection(11, 18, 11, 22));326});327328test('Problems with nested snippet insertion #39594', function () {329ctrl = instaService.createInstance(SnippetController2, editor);330331model.setValue('');332editor.setSelection(new Selection(1, 1, 1, 1));333334ctrl.insert('$1 = ConvertTo-Json $1');335assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(1, 19, 1, 19));336337editor.setSelection(new Selection(1, 19, 1, 19));338339// snippet mode should stop because $1 has two occurrences340// and we only have one selection left341assertContextKeys(contextKeys, false, false, false);342});343344test('Problems with nested snippet insertion #39594 (part2)', function () {345// ensure selection-change-to-cancel logic isn't too aggressive346ctrl = instaService.createInstance(SnippetController2, editor);347348model.setValue('a-\naaa-');349editor.setSelections([new Selection(2, 5, 2, 5), new Selection(1, 3, 1, 3)]);350351ctrl.insert('log($1);$0');352assertSelections(editor, new Selection(2, 9, 2, 9), new Selection(1, 7, 1, 7));353assertContextKeys(contextKeys, true, false, true);354});355356test('“Nested” snippets terminating abruptly in VSCode 1.19.2. #42012', function () {357358ctrl = instaService.createInstance(SnippetController2, editor);359model.setValue('');360editor.setSelection(new Selection(1, 1, 1, 1));361ctrl.insert('var ${2:${1:name}} = ${1:name} + 1;${0}');362363assertSelections(editor, new Selection(1, 5, 1, 9), new Selection(1, 12, 1, 16));364assertContextKeys(contextKeys, true, false, true);365366ctrl.next();367assertContextKeys(contextKeys, true, true, true);368});369370test('Placeholders order #58267', function () {371372ctrl = instaService.createInstance(SnippetController2, editor);373model.setValue('');374editor.setSelection(new Selection(1, 1, 1, 1));375ctrl.insert('\\pth{$1}$0');376377assertSelections(editor, new Selection(1, 6, 1, 6));378assertContextKeys(contextKeys, true, false, true);379380ctrl.insert('\\itv{${1:left}}{${2:right}}{${3:left_value}}{${4:right_value}}$0');381assertSelections(editor, new Selection(1, 11, 1, 15));382383ctrl.next();384assertSelections(editor, new Selection(1, 17, 1, 22));385386ctrl.next();387assertSelections(editor, new Selection(1, 24, 1, 34));388389ctrl.next();390assertSelections(editor, new Selection(1, 36, 1, 47));391392ctrl.next();393assertSelections(editor, new Selection(1, 48, 1, 48));394395ctrl.next();396assertSelections(editor, new Selection(1, 49, 1, 49));397assertContextKeys(contextKeys, false, false, false);398});399400test('Must tab through deleted tab stops in snippets #31619', function () {401ctrl = instaService.createInstance(SnippetController2, editor);402model.setValue('');403editor.setSelection(new Selection(1, 1, 1, 1));404ctrl.insert('foo${1:a${2:bar}baz}end$0');405assertSelections(editor, new Selection(1, 4, 1, 11));406407editor.trigger('test', Handler.Cut, null);408assertSelections(editor, new Selection(1, 4, 1, 4));409410ctrl.next();411assertSelections(editor, new Selection(1, 7, 1, 7));412assertContextKeys(contextKeys, false, false, false);413});414415test('Cancelling snippet mode should discard added cursors #68512 (soft cancel)', function () {416ctrl = instaService.createInstance(SnippetController2, editor);417model.setValue('');418editor.setSelection(new Selection(1, 1, 1, 1));419420ctrl.insert('.REGION ${2:FUNCTION_NAME}\nCREATE.FUNCTION ${1:VOID} ${2:FUNCTION_NAME}(${3:})\n\t${4:}\nEND\n.ENDREGION$0');421assertSelections(editor, new Selection(2, 17, 2, 21));422423ctrl.next();424assertSelections(editor, new Selection(1, 9, 1, 22), new Selection(2, 22, 2, 35));425assertContextKeys(contextKeys, true, true, true);426427editor.setSelections([new Selection(1, 22, 1, 22), new Selection(2, 35, 2, 35)]);428assertContextKeys(contextKeys, true, true, true);429430editor.setSelections([new Selection(2, 1, 2, 1), new Selection(2, 36, 2, 36)]);431assertContextKeys(contextKeys, false, false, false);432assertSelections(editor, new Selection(2, 1, 2, 1), new Selection(2, 36, 2, 36));433});434435test('Cancelling snippet mode should discard added cursors #68512 (hard cancel)', function () {436ctrl = instaService.createInstance(SnippetController2, editor);437model.setValue('');438editor.setSelection(new Selection(1, 1, 1, 1));439440ctrl.insert('.REGION ${2:FUNCTION_NAME}\nCREATE.FUNCTION ${1:VOID} ${2:FUNCTION_NAME}(${3:})\n\t${4:}\nEND\n.ENDREGION$0');441assertSelections(editor, new Selection(2, 17, 2, 21));442443ctrl.next();444assertSelections(editor, new Selection(1, 9, 1, 22), new Selection(2, 22, 2, 35));445assertContextKeys(contextKeys, true, true, true);446447editor.setSelections([new Selection(1, 22, 1, 22), new Selection(2, 35, 2, 35)]);448assertContextKeys(contextKeys, true, true, true);449450ctrl.cancel(true);451assertContextKeys(contextKeys, false, false, false);452assertSelections(editor, new Selection(1, 22, 1, 22));453});454455test('User defined snippet tab stops ignored #72862', function () {456ctrl = instaService.createInstance(SnippetController2, editor);457model.setValue('');458editor.setSelection(new Selection(1, 1, 1, 1));459460ctrl.insert('export default $1');461assertContextKeys(contextKeys, true, false, true);462});463464test('Optional tabstop in snippets #72358', function () {465ctrl = instaService.createInstance(SnippetController2, editor);466model.setValue('');467editor.setSelection(new Selection(1, 1, 1, 1));468469ctrl.insert('${1:prop: {$2\\},}\nmore$0');470assertContextKeys(contextKeys, true, false, true);471472assertSelections(editor, new Selection(1, 1, 1, 10));473editor.trigger('test', Handler.Cut, {});474475assertSelections(editor, new Selection(1, 1, 1, 1));476477ctrl.next();478assertSelections(editor, new Selection(2, 5, 2, 5));479assertContextKeys(contextKeys, false, false, false);480});481482test('issue #90135: confusing trim whitespace edits', function () {483ctrl = instaService.createInstance(SnippetController2, editor);484model.setValue('');485editor.runCommand(CoreEditingCommands.Tab, null);486487ctrl.insert('\nfoo');488assertSelections(editor, new Selection(2, 8, 2, 8));489});490491test('issue #145727: insertSnippet can put snippet selections in wrong positions (1 of 2)', function () {492ctrl = instaService.createInstance(SnippetController2, editor);493model.setValue('');494editor.runCommand(CoreEditingCommands.Tab, null);495496ctrl.insert('\naProperty: aClass<${2:boolean}> = new aClass<${2:boolean}>();\n', { adjustWhitespace: false });497assertSelections(editor, new Selection(2, 19, 2, 26), new Selection(2, 41, 2, 48));498});499500test('issue #145727: insertSnippet can put snippet selections in wrong positions (2 of 2)', function () {501ctrl = instaService.createInstance(SnippetController2, editor);502model.setValue('');503editor.runCommand(CoreEditingCommands.Tab, null);504505ctrl.insert('\naProperty: aClass<${2:boolean}> = new aClass<${2:boolean}>();\n');506// This will insert \n aProperty....507assertSelections(editor, new Selection(2, 23, 2, 30), new Selection(2, 45, 2, 52));508});509510test('leading TAB by snippets won\'t replace by spaces #101870', function () {511ctrl = instaService.createInstance(SnippetController2, editor);512model.setValue('');513model.updateOptions({ insertSpaces: true, tabSize: 4 });514ctrl.insert('\tHello World\n\tNew Line');515assert.strictEqual(model.getValue(), ' Hello World\n New Line');516});517518test('leading TAB by snippets won\'t replace by spaces #101870 (part 2)', function () {519ctrl = instaService.createInstance(SnippetController2, editor);520model.setValue('');521model.updateOptions({ insertSpaces: true, tabSize: 4 });522ctrl.insert('\tHello World\n\tNew Line\n${1:\tmore}');523assert.strictEqual(model.getValue(), ' Hello World\n New Line\n more');524});525526test.skip('Snippet transformation does not work after inserting variable using intellisense, #112362', function () {527528{529// HAPPY - no nested snippet530ctrl = instaService.createInstance(SnippetController2, editor);531model.setValue('');532model.updateOptions({ insertSpaces: true, tabSize: 4 });533ctrl.insert('$1\n\n${1/([A-Za-z0-9]+): ([A-Za-z]+).*/$1: \'$2\',/gm}');534535assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(3, 1, 3, 1));536editor.trigger('test', 'type', { text: 'foo: number;' });537ctrl.next();538assert.strictEqual(model.getValue(), `foo: number;\n\nfoo: 'number',`);539}540541ctrl = instaService.createInstance(SnippetController2, editor);542model.setValue('');543model.updateOptions({ insertSpaces: true, tabSize: 4 });544ctrl.insert('$1\n\n${1/([A-Za-z0-9]+): ([A-Za-z]+).*/$1: \'$2\',/gm}');545546assertSelections(editor, new Selection(1, 1, 1, 1), new Selection(3, 1, 3, 1));547editor.trigger('test', 'type', { text: 'foo: ' });548ctrl.insert('number;');549ctrl.next();550assert.strictEqual(model.getValue(), `foo: number;\n\nfoo: 'number',`);551// editor.trigger('test', 'type', { text: ';' });552});553554suite('createEditsAndSnippetsFromEdits', function () {555556test('apply, tab, done', function () {557558ctrl = instaService.createInstance(SnippetController2, editor);559560model.setValue('foo("bar")');561562ctrl.apply([563{ range: new Range(1, 5, 1, 10), template: '$1' },564{ range: new Range(1, 1, 1, 1), template: 'const ${1:new_const} = "bar";\n' }565]);566567assert.strictEqual(model.getValue(), "const new_const = \"bar\";\nfoo(new_const)");568assertContextKeys(contextKeys, true, false, true);569assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 7, 1, 16), new Selection(2, 5, 2, 14)]);570571ctrl.next();572assertContextKeys(contextKeys, false, false, false);573assert.deepStrictEqual(editor.getSelections(), [new Selection(2, 14, 2, 14)]);574});575576test('apply, tab, done with special final tabstop', function () {577578model.setValue('foo("bar")');579580ctrl = instaService.createInstance(SnippetController2, editor);581ctrl.apply([582{ range: new Range(1, 5, 1, 10), template: '$1' },583{ range: new Range(1, 1, 1, 1), template: 'const ${1:new_const}$0 = "bar";\n' }584]);585586assert.strictEqual(model.getValue(), "const new_const = \"bar\";\nfoo(new_const)");587assertContextKeys(contextKeys, true, false, true);588assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 7, 1, 16), new Selection(2, 5, 2, 14)]);589590ctrl.next();591assertContextKeys(contextKeys, false, false, false);592assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 16, 1, 16)]);593});594595test('apply, tab, tab, done', function () {596597model.setValue('foo\nbar');598599ctrl = instaService.createInstance(SnippetController2, editor);600ctrl.apply([601{ range: new Range(1, 4, 1, 4), template: '${3}' },602{ range: new Range(2, 4, 2, 4), template: '$3' },603{ range: new Range(1, 1, 1, 1), template: '### ${2:Header}\n' }604]);605606assert.strictEqual(model.getValue(), "### Header\nfoo\nbar");607assert.deepStrictEqual(getContextState(), { inSnippet: true, hasPrev: false, hasNext: true });608assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 11)]);609610ctrl.next();611assert.deepStrictEqual(getContextState(), { inSnippet: true, hasPrev: true, hasNext: true });612assert.deepStrictEqual(editor.getSelections(), [new Selection(2, 4, 2, 4), new Selection(3, 4, 3, 4)]);613614ctrl.next();615assert.deepStrictEqual(getContextState(), { inSnippet: false, hasPrev: false, hasNext: false });616assert.deepStrictEqual(editor.getSelections(), [new Selection(3, 4, 3, 4)]);617});618619test('nested into apply works', function () {620621ctrl = instaService.createInstance(SnippetController2, editor);622model.setValue('onetwo');623624editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]);625626ctrl.apply([{627range: new Range(1, 7, 1, 7),628template: '$0${1:three}'629}]);630631assert.strictEqual(model.getValue(), 'onetwothree');632assert.deepStrictEqual(getContextState(), { inSnippet: true, hasPrev: false, hasNext: true });633assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 7, 1, 12)]);634635ctrl.insert('foo$1bar$1');636assert.strictEqual(model.getValue(), 'onetwofoobar');637assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 10, 1, 10), new Selection(1, 13, 1, 13)]);638assert.deepStrictEqual(getContextState(), ({ inSnippet: true, hasPrev: false, hasNext: true }));639640ctrl.next();641assert.deepStrictEqual(getContextState(), ({ inSnippet: true, hasPrev: true, hasNext: true }));642assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 13, 1, 13)]);643644ctrl.next();645assert.deepStrictEqual(getContextState(), { inSnippet: false, hasPrev: false, hasNext: false });646assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 7, 1, 7)]);647648});649650test('nested into insert abort "outer" snippet', function () {651652ctrl = instaService.createInstance(SnippetController2, editor);653model.setValue('one\ntwo');654655editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]);656657ctrl.insert('foo${1:bar}bazz${1:bang}');658assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 7), new Selection(1, 11, 1, 14), new Selection(2, 4, 2, 7), new Selection(2, 11, 2, 14)]);659assert.deepStrictEqual(getContextState(), { inSnippet: true, hasPrev: false, hasNext: true });660661ctrl.apply([{662range: new Range(1, 4, 1, 7),663template: '$0A'664}]);665666assert.strictEqual(model.getValue(), 'fooAbazzbarone\nfoobarbazzbartwo');667assert.deepStrictEqual(getContextState(), { inSnippet: false, hasPrev: false, hasNext: false });668assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]);669});670671test('nested into "insert" abort "outer" snippet (2)', function () {672673ctrl = instaService.createInstance(SnippetController2, editor);674model.setValue('one\ntwo');675676editor.setSelections([new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]);677678ctrl.insert('foo${1:bar}bazz${1:bang}');679assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 4, 1, 7), new Selection(1, 11, 1, 14), new Selection(2, 4, 2, 7), new Selection(2, 11, 2, 14)]);680assert.deepStrictEqual(getContextState(), { inSnippet: true, hasPrev: false, hasNext: true });681682const edits = [{683range: new Range(1, 4, 1, 7),684template: 'A'685}, {686range: new Range(1, 11, 1, 14),687template: 'B'688}, {689range: new Range(2, 4, 2, 7),690template: 'C'691}, {692range: new Range(2, 11, 2, 14),693template: 'D'694}];695ctrl.apply(edits);696697assert.strictEqual(model.getValue(), "fooAbazzBone\nfooCbazzDtwo");698assert.deepStrictEqual(getContextState(), { inSnippet: false, hasPrev: false, hasNext: false });699assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 5, 1, 5), new Selection(1, 10, 1, 10), new Selection(2, 5, 2, 5), new Selection(2, 10, 2, 10)]);700});701});702703test('Bug: cursor position $0 with user snippets #163808', function () {704705ctrl = instaService.createInstance(SnippetController2, editor);706model.setValue('');707708ctrl.insert('<Element1 Attr1="foo" $1>\n <Element2 Attr1="$2"/>\n$0"\n</Element1>');709assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 23, 1, 23)]);710711ctrl.insert('Qualifier="$0"');712assert.strictEqual(model.getValue(), '<Element1 Attr1="foo" Qualifier="">\n <Element2 Attr1=""/>\n"\n</Element1>');713assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 34, 1, 34)]);714715});716717test('EOL-Sequence (CRLF) shifts tab stop in isFileTemplate snippets #167386', function () {718ctrl = instaService.createInstance(SnippetController2, editor);719model.setValue('');720model.setEOL(EndOfLineSequence.CRLF);721722ctrl.apply([{723range: model.getFullModelRange(),724template: 'line 54321${1:FOO}\nline 54321${1:FOO}\n(no tab stop)\nline 54321${1:FOO}\nline 54321'725}]);726727assert.deepStrictEqual(editor.getSelections(), [new Selection(1, 11, 1, 14), new Selection(2, 11, 2, 14), new Selection(4, 11, 4, 14)]);728729});730731test('"Surround With" code action snippets use incorrect indentation levels and styles #169319', function () {732model.setValue('function foo(f, x, condition) {\n f();\n return x;\n}');733const sel = new Range(2, 5, 3, 14);734editor.setSelection(sel);735ctrl = instaService.createInstance(SnippetController2, editor);736ctrl.apply([{737range: sel,738template: 'if (${1:condition}) {\n\t$TM_SELECTED_TEXT$0\n}'739}]);740741assert.strictEqual(model.getValue(), `function foo(f, x, condition) {\n if (condition) {\n f();\n return x;\n }\n}`);742});743});744745746