Path: blob/main/src/vs/editor/contrib/snippet/test/browser/snippetParser.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 { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';6import { Choice, FormatString, Marker, Placeholder, Scanner, SnippetParser, Text, TextmateSnippet, TokenType, Transform, Variable } from '../../browser/snippetParser.js';78suite('SnippetParser', () => {910ensureNoDisposablesAreLeakedInTestSuite();1112test('Scanner', () => {1314const scanner = new Scanner();15assert.strictEqual(scanner.next().type, TokenType.EOF);1617scanner.text('abc');18assert.strictEqual(scanner.next().type, TokenType.VariableName);19assert.strictEqual(scanner.next().type, TokenType.EOF);2021scanner.text('{{abc}}');22assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);23assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);24assert.strictEqual(scanner.next().type, TokenType.VariableName);25assert.strictEqual(scanner.next().type, TokenType.CurlyClose);26assert.strictEqual(scanner.next().type, TokenType.CurlyClose);27assert.strictEqual(scanner.next().type, TokenType.EOF);2829scanner.text('abc() ');30assert.strictEqual(scanner.next().type, TokenType.VariableName);31assert.strictEqual(scanner.next().type, TokenType.Format);32assert.strictEqual(scanner.next().type, TokenType.EOF);3334scanner.text('abc 123');35assert.strictEqual(scanner.next().type, TokenType.VariableName);36assert.strictEqual(scanner.next().type, TokenType.Format);37assert.strictEqual(scanner.next().type, TokenType.Int);38assert.strictEqual(scanner.next().type, TokenType.EOF);3940scanner.text('$foo');41assert.strictEqual(scanner.next().type, TokenType.Dollar);42assert.strictEqual(scanner.next().type, TokenType.VariableName);43assert.strictEqual(scanner.next().type, TokenType.EOF);4445scanner.text('$foo_bar');46assert.strictEqual(scanner.next().type, TokenType.Dollar);47assert.strictEqual(scanner.next().type, TokenType.VariableName);48assert.strictEqual(scanner.next().type, TokenType.EOF);4950scanner.text('$foo-bar');51assert.strictEqual(scanner.next().type, TokenType.Dollar);52assert.strictEqual(scanner.next().type, TokenType.VariableName);53assert.strictEqual(scanner.next().type, TokenType.Dash);54assert.strictEqual(scanner.next().type, TokenType.VariableName);55assert.strictEqual(scanner.next().type, TokenType.EOF);5657scanner.text('${foo}');58assert.strictEqual(scanner.next().type, TokenType.Dollar);59assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);60assert.strictEqual(scanner.next().type, TokenType.VariableName);61assert.strictEqual(scanner.next().type, TokenType.CurlyClose);62assert.strictEqual(scanner.next().type, TokenType.EOF);6364scanner.text('${1223:foo}');65assert.strictEqual(scanner.next().type, TokenType.Dollar);66assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);67assert.strictEqual(scanner.next().type, TokenType.Int);68assert.strictEqual(scanner.next().type, TokenType.Colon);69assert.strictEqual(scanner.next().type, TokenType.VariableName);70assert.strictEqual(scanner.next().type, TokenType.CurlyClose);71assert.strictEqual(scanner.next().type, TokenType.EOF);7273scanner.text('\\${}');74assert.strictEqual(scanner.next().type, TokenType.Backslash);75assert.strictEqual(scanner.next().type, TokenType.Dollar);76assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);77assert.strictEqual(scanner.next().type, TokenType.CurlyClose);7879scanner.text('${foo/regex/format/option}');80assert.strictEqual(scanner.next().type, TokenType.Dollar);81assert.strictEqual(scanner.next().type, TokenType.CurlyOpen);82assert.strictEqual(scanner.next().type, TokenType.VariableName);83assert.strictEqual(scanner.next().type, TokenType.Forwardslash);84assert.strictEqual(scanner.next().type, TokenType.VariableName);85assert.strictEqual(scanner.next().type, TokenType.Forwardslash);86assert.strictEqual(scanner.next().type, TokenType.VariableName);87assert.strictEqual(scanner.next().type, TokenType.Forwardslash);88assert.strictEqual(scanner.next().type, TokenType.VariableName);89assert.strictEqual(scanner.next().type, TokenType.CurlyClose);90assert.strictEqual(scanner.next().type, TokenType.EOF);91});9293function assertText(value: string, expected: string) {94const actual = SnippetParser.asInsertText(value);95assert.strictEqual(actual, expected);96}9798function assertMarker(input: TextmateSnippet | Marker[] | string, ...ctors: Function[]) {99let marker: Marker[];100if (input instanceof TextmateSnippet) {101marker = [...input.children];102} else if (typeof input === 'string') {103const p = new SnippetParser();104marker = p.parse(input).children;105} else {106marker = [...input];107}108while (marker.length > 0) {109const m = marker.pop();110const ctor = ctors.pop()!;111assert.ok(m instanceof ctor);112}113assert.strictEqual(marker.length, ctors.length);114assert.strictEqual(marker.length, 0);115}116117function assertTextAndMarker(value: string, escaped: string, ...ctors: Function[]) {118assertText(value, escaped);119assertMarker(value, ...ctors);120}121122function assertEscaped(value: string, expected: string) {123const actual = SnippetParser.escape(value);124assert.strictEqual(actual, expected);125}126127test('Parser, escaped', function () {128assertEscaped('foo$0', 'foo\\$0');129assertEscaped('foo\\$0', 'foo\\\\\\$0');130assertEscaped('f$1oo$0', 'f\\$1oo\\$0');131assertEscaped('${1:foo}$0', '\\${1:foo\\}\\$0');132assertEscaped('$', '\\$');133});134135test('Parser, text', () => {136assertText('$', '$');137assertText('\\\\$', '\\$');138assertText('{', '{');139assertText('\\}', '}');140assertText('\\abc', '\\abc');141assertText('foo${f:\\}}bar', 'foo}bar');142assertText('\\{', '\\{');143assertText('I need \\\\\\$', 'I need \\$');144assertText('\\', '\\');145assertText('\\{{', '\\{{');146assertText('{{', '{{');147assertText('{{dd', '{{dd');148assertText('}}', '}}');149assertText('ff}}', 'ff}}');150151assertText('farboo', 'farboo');152assertText('far{{}}boo', 'far{{}}boo');153assertText('far{{123}}boo', 'far{{123}}boo');154assertText('far\\{{123}}boo', 'far\\{{123}}boo');155assertText('far{{id:bern}}boo', 'far{{id:bern}}boo');156assertText('far{{id:bern {{basel}}}}boo', 'far{{id:bern {{basel}}}}boo');157assertText('far{{id:bern {{id:basel}}}}boo', 'far{{id:bern {{id:basel}}}}boo');158assertText('far{{id:bern {{id2:basel}}}}boo', 'far{{id:bern {{id2:basel}}}}boo');159});160161162test('Parser, TM text', () => {163assertTextAndMarker('foo${1:bar}}', 'foobar}', Text, Placeholder, Text);164assertTextAndMarker('foo${1:bar}${2:foo}}', 'foobarfoo}', Text, Placeholder, Placeholder, Text);165166assertTextAndMarker('foo${1:bar\\}${2:foo}}', 'foobar}foo', Text, Placeholder);167168const [, placeholder] = new SnippetParser().parse('foo${1:bar\\}${2:foo}}').children;169const { children } = (<Placeholder>placeholder);170171assert.strictEqual((<Placeholder>placeholder).index, 1);172assert.ok(children[0] instanceof Text);173assert.strictEqual(children[0].toString(), 'bar}');174assert.ok(children[1] instanceof Placeholder);175assert.strictEqual(children[1].toString(), 'foo');176});177178test('Parser, placeholder', () => {179assertTextAndMarker('farboo', 'farboo', Text);180assertTextAndMarker('far{{}}boo', 'far{{}}boo', Text);181assertTextAndMarker('far{{123}}boo', 'far{{123}}boo', Text);182assertTextAndMarker('far\\{{123}}boo', 'far\\{{123}}boo', Text);183});184185test('Parser, literal code', () => {186assertTextAndMarker('far`123`boo', 'far`123`boo', Text);187assertTextAndMarker('far\\`123\\`boo', 'far\\`123\\`boo', Text);188});189190test('Parser, variables/tabstop', () => {191assertTextAndMarker('$far-boo', '-boo', Variable, Text);192assertTextAndMarker('\\$far-boo', '$far-boo', Text);193assertTextAndMarker('far$farboo', 'far', Text, Variable);194assertTextAndMarker('far${farboo}', 'far', Text, Variable);195assertTextAndMarker('$123', '', Placeholder);196assertTextAndMarker('$farboo', '', Variable);197assertTextAndMarker('$far12boo', '', Variable);198assertTextAndMarker('000_${far}_000', '000__000', Text, Variable, Text);199assertTextAndMarker('FFF_${TM_SELECTED_TEXT}_FFF$0', 'FFF__FFF', Text, Variable, Text, Placeholder);200});201202test('Parser, variables/placeholder with defaults', () => {203assertTextAndMarker('${name:value}', 'value', Variable);204assertTextAndMarker('${1:value}', 'value', Placeholder);205assertTextAndMarker('${1:bar${2:foo}bar}', 'barfoobar', Placeholder);206207assertTextAndMarker('${name:value', '${name:value', Text);208assertTextAndMarker('${1:bar${2:foobar}', '${1:barfoobar', Text, Placeholder);209});210211test('Parser, variable transforms', function () {212assertTextAndMarker('${foo///}', '', Variable);213assertTextAndMarker('${foo/regex/format/gmi}', '', Variable);214assertTextAndMarker('${foo/([A-Z][a-z])/format/}', '', Variable);215216// invalid regex217assertTextAndMarker('${foo/([A-Z][a-z])/format/GMI}', '${foo/([A-Z][a-z])/format/GMI}', Text);218assertTextAndMarker('${foo/([A-Z][a-z])/format/funky}', '${foo/([A-Z][a-z])/format/funky}', Text);219assertTextAndMarker('${foo/([A-Z][a-z]/format/}', '${foo/([A-Z][a-z]/format/}', Text);220221// tricky regex222assertTextAndMarker('${foo/m\\/atch/$1/i}', '', Variable);223assertMarker('${foo/regex\/format/options}', Text);224225// incomplete226assertTextAndMarker('${foo///', '${foo///', Text);227assertTextAndMarker('${foo/regex/format/options', '${foo/regex/format/options', Text);228229// format string230assertMarker('${foo/.*/${0:fooo}/i}', Variable);231assertMarker('${foo/.*/${1}/i}', Variable);232assertMarker('${foo/.*/$1/i}', Variable);233assertMarker('${foo/.*/This-$1-encloses/i}', Variable);234assertMarker('${foo/.*/complex${1:else}/i}', Variable);235assertMarker('${foo/.*/complex${1:-else}/i}', Variable);236assertMarker('${foo/.*/complex${1:+if}/i}', Variable);237assertMarker('${foo/.*/complex${1:?if:else}/i}', Variable);238assertMarker('${foo/.*/complex${1:/upcase}/i}', Variable);239240});241242test('Parser, placeholder transforms', function () {243assertTextAndMarker('${1///}', '', Placeholder);244assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder);245assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder);246247// tricky regex248assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder);249assertMarker('${1/regex\/format/options}', Text);250251// incomplete252assertTextAndMarker('${1///', '${1///', Text);253assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text);254});255256test('No way to escape forward slash in snippet regex #36715', function () {257assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable);258});259260test('No way to escape forward slash in snippet format section #37562', function () {261assertMarker('${TM_SELECTED_TEXT/a/\\/$1/g}', Variable);262assertMarker('${TM_SELECTED_TEXT/a/in\\/$1ner/g}', Variable);263assertMarker('${TM_SELECTED_TEXT/a/end\\//g}', Variable);264});265266test('Parser, placeholder with choice', () => {267268assertTextAndMarker('${1|one,two,three|}', 'one', Placeholder);269assertTextAndMarker('${1|one|}', 'one', Placeholder);270assertTextAndMarker('${1|one1,two2|}', 'one1', Placeholder);271assertTextAndMarker('${1|one1\\,two2|}', 'one1,two2', Placeholder);272assertTextAndMarker('${1|one1\\|two2|}', 'one1|two2', Placeholder);273assertTextAndMarker('${1|one1\\atwo2|}', 'one1\\atwo2', Placeholder);274assertTextAndMarker('${1|one,two,three,|}', '${1|one,two,three,|}', Text);275assertTextAndMarker('${1|one,', '${1|one,', Text);276277const snippet = new SnippetParser().parse('${1|one,two,three|}');278const expected: ((m: Marker) => boolean)[] = [279m => m instanceof Placeholder,280m => m instanceof Choice && m.options.length === 3 && m.options.every(x => x instanceof Text),281];282snippet.walk(marker => {283assert.ok(expected.shift()!(marker));284return true;285});286});287288test('Snippet choices: unable to escape comma and pipe, #31521', function () {289assertTextAndMarker('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(not, not);', Text, Placeholder, Text);290});291292test('Marker, toTextmateString()', function () {293294function assertTextsnippetString(input: string, expected: string): void {295const snippet = new SnippetParser().parse(input);296const actual = snippet.toTextmateString();297assert.strictEqual(actual, expected);298}299300assertTextsnippetString('$1', '$1');301assertTextsnippetString('\\$1', '\\$1');302assertTextsnippetString('console.log(${1|not\\, not, five, 5, 1 23|});', 'console.log(${1|not\\, not, five, 5, 1 23|});');303assertTextsnippetString('console.log(${1|not\\, not, \\| five, 5, 1 23|});', 'console.log(${1|not\\, not, \\| five, 5, 1 23|});');304assertTextsnippetString('${1|cho\\,ices,wi\\|th,esc\\\\aping,chall\\\\\\,enges|}', '${1|cho\\,ices,wi\\|th,esc\\\\aping,chall\\\\\\,enges|}');305assertTextsnippetString('this is text', 'this is text');306assertTextsnippetString('this ${1:is ${2:nested with $var}}', 'this ${1:is ${2:nested with ${var}}}');307assertTextsnippetString('this ${1:is ${2:nested with $var}}}', 'this ${1:is ${2:nested with ${var}}}\\}');308});309310test('Marker, toTextmateString() <-> identity', function () {311312function assertIdent(input: string): void {313// full loop: (1) parse input, (2) generate textmate string, (3) parse, (4) ensure both trees are equal314const snippet = new SnippetParser().parse(input);315const input2 = snippet.toTextmateString();316const snippet2 = new SnippetParser().parse(input2);317318function checkCheckChildren(marker1: Marker, marker2: Marker) {319assert.ok(marker1 instanceof Object.getPrototypeOf(marker2).constructor);320assert.ok(marker2 instanceof Object.getPrototypeOf(marker1).constructor);321322assert.strictEqual(marker1.children.length, marker2.children.length);323assert.strictEqual(marker1.toString(), marker2.toString());324325for (let i = 0; i < marker1.children.length; i++) {326checkCheckChildren(marker1.children[i], marker2.children[i]);327}328}329330checkCheckChildren(snippet, snippet2);331}332333assertIdent('$1');334assertIdent('\\$1');335assertIdent('console.log(${1|not\\, not, five, 5, 1 23|});');336assertIdent('console.log(${1|not\\, not, \\| five, 5, 1 23|});');337assertIdent('this is text');338assertIdent('this ${1:is ${2:nested with $var}}');339assertIdent('this ${1:is ${2:nested with $var}}}');340assertIdent('this ${1:is ${2:nested with $var}} and repeating $1');341});342343test('Parser, choise marker', () => {344const { placeholders } = new SnippetParser().parse('${1|one,two,three|}');345346assert.strictEqual(placeholders.length, 1);347assert.ok(placeholders[0].choice instanceof Choice);348assert.ok(placeholders[0].children[0] instanceof Choice);349assert.strictEqual((<Choice>placeholders[0].children[0]).options.length, 3);350351assertText('${1|one,two,three|}', 'one');352assertText('\\${1|one,two,three|}', '${1|one,two,three|}');353assertText('${1\\|one,two,three|}', '${1\\|one,two,three|}');354assertText('${1||}', '${1||}');355});356357test('Backslash character escape in choice tabstop doesn\'t work #58494', function () {358359const { placeholders } = new SnippetParser().parse('${1|\\,,},$,\\|,\\\\|}');360assert.strictEqual(placeholders.length, 1);361assert.ok(placeholders[0].choice instanceof Choice);362});363364test('Parser, only textmate', () => {365const p = new SnippetParser();366assertMarker(p.parse('far{{}}boo'), Text);367assertMarker(p.parse('far{{123}}boo'), Text);368assertMarker(p.parse('far\\{{123}}boo'), Text);369370assertMarker(p.parse('far$0boo'), Text, Placeholder, Text);371assertMarker(p.parse('far${123}boo'), Text, Placeholder, Text);372assertMarker(p.parse('far\\${123}boo'), Text);373});374375test('Parser, real world', () => {376let marker = new SnippetParser().parse('console.warn(${1: $TM_SELECTED_TEXT })').children;377378assert.strictEqual(marker[0].toString(), 'console.warn(');379assert.ok(marker[1] instanceof Placeholder);380assert.strictEqual(marker[2].toString(), ')');381382const placeholder = <Placeholder>marker[1];383assert.strictEqual(placeholder.index, 1);384assert.strictEqual(placeholder.children.length, 3);385assert.ok(placeholder.children[0] instanceof Text);386assert.ok(placeholder.children[1] instanceof Variable);387assert.ok(placeholder.children[2] instanceof Text);388assert.strictEqual(placeholder.children[0].toString(), ' ');389assert.strictEqual(placeholder.children[1].toString(), '');390assert.strictEqual(placeholder.children[2].toString(), ' ');391392const nestedVariable = <Variable>placeholder.children[1];393assert.strictEqual(nestedVariable.name, 'TM_SELECTED_TEXT');394assert.strictEqual(nestedVariable.children.length, 0);395396marker = new SnippetParser().parse('$TM_SELECTED_TEXT').children;397assert.strictEqual(marker.length, 1);398assert.ok(marker[0] instanceof Variable);399});400401test('Parser, transform example', () => {402const { children } = new SnippetParser().parse('${1:name} : ${2:type}${3/\\s:=(.*)/${1:+ :=}${1}/};\n$0');403404//${1:name}405assert.ok(children[0] instanceof Placeholder);406assert.strictEqual(children[0].children.length, 1);407assert.strictEqual(children[0].children[0].toString(), 'name');408assert.strictEqual((<Placeholder>children[0]).transform, undefined);409410// :411assert.ok(children[1] instanceof Text);412assert.strictEqual(children[1].toString(), ' : ');413414//${2:type}415assert.ok(children[2] instanceof Placeholder);416assert.strictEqual(children[2].children.length, 1);417assert.strictEqual(children[2].children[0].toString(), 'type');418419//${3/\\s:=(.*)/${1:+ :=}${1}/}420assert.ok(children[3] instanceof Placeholder);421assert.strictEqual(children[3].children.length, 0);422assert.notStrictEqual((<Placeholder>children[3]).transform, undefined);423const transform = (<Placeholder>children[3]).transform!;424assert.deepStrictEqual(transform.regexp, /\s:=(.*)/);425assert.strictEqual(transform.children.length, 2);426assert.ok(transform.children[0] instanceof FormatString);427assert.strictEqual((<FormatString>transform.children[0]).index, 1);428assert.strictEqual((<FormatString>transform.children[0]).ifValue, ' :=');429assert.ok(transform.children[1] instanceof FormatString);430assert.strictEqual((<FormatString>transform.children[1]).index, 1);431assert.ok(children[4] instanceof Text);432assert.strictEqual(children[4].toString(), ';\n');433434});435436// TODO @jrieken making this strictEqul causes circular json conversion errors437test('Parser, default placeholder values', () => {438439assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder);440441const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err}`, error:$1').children;442443assert.strictEqual((<Placeholder>p1).index, 1);444assert.strictEqual((<Placeholder>p1).children.length, 1);445assert.strictEqual((<Text>(<Placeholder>p1).children[0]).toString(), 'err');446447assert.strictEqual((<Placeholder>p2).index, 1);448assert.strictEqual((<Placeholder>p2).children.length, 1);449assert.strictEqual((<Text>(<Placeholder>p2).children[0]).toString(), 'err');450});451452// TODO @jrieken making this strictEqul causes circular json conversion errors453test('Parser, default placeholder values and one transform', () => {454455assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder);456457const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children;458459assert.strictEqual((<Placeholder>p3).index, 1);460assert.strictEqual((<Placeholder>p3).children.length, 1);461assert.strictEqual((<Text>(<Placeholder>p3).children[0]).toString(), 'err');462assert.strictEqual((<Placeholder>p3).transform, undefined);463464assert.strictEqual((<Placeholder>p4).index, 1);465assert.strictEqual((<Placeholder>p4).children.length, 1);466assert.strictEqual((<Text>(<Placeholder>p4).children[0]).toString(), 'err');467assert.notStrictEqual((<Placeholder>p4).transform, undefined);468});469470test('Repeated snippet placeholder should always inherit, #31040', function () {471assertText('${1:foo}-abc-$1', 'foo-abc-foo');472assertText('${1:foo}-abc-${1}', 'foo-abc-foo');473assertText('${1:foo}-abc-${1:bar}', 'foo-abc-foo');474assertText('${1}-abc-${1:foo}', 'foo-abc-foo');475});476477test('backspace esapce in TM only, #16212', () => {478const actual = SnippetParser.asInsertText('Foo \\\\${abc}bar');479assert.strictEqual(actual, 'Foo \\bar');480});481482test('colon as variable/placeholder value, #16717', () => {483let actual = SnippetParser.asInsertText('${TM_SELECTED_TEXT:foo:bar}');484assert.strictEqual(actual, 'foo:bar');485486actual = SnippetParser.asInsertText('${1:foo:bar}');487assert.strictEqual(actual, 'foo:bar');488});489490test('incomplete placeholder', () => {491assertTextAndMarker('${1:}', '', Placeholder);492});493494test('marker#len', () => {495496function assertLen(template: string, ...lengths: number[]): void {497const snippet = new SnippetParser().parse(template, true);498snippet.walk(m => {499const expected = lengths.shift();500assert.strictEqual(m.len(), expected);501return true;502});503assert.strictEqual(lengths.length, 0);504}505506assertLen('text$0', 4, 0);507assertLen('$1text$0', 0, 4, 0);508assertLen('te$1xt$0', 2, 0, 2, 0);509assertLen('errorContext: `${1:err}`, error: $0', 15, 0, 3, 10, 0);510assertLen('errorContext: `${1:err}`, error: $1$0', 15, 0, 3, 10, 0, 3, 0);511assertLen('$TM_SELECTED_TEXT$0', 0, 0);512assertLen('${TM_SELECTED_TEXT:def}$0', 0, 3, 0);513});514515test('parser, parent node', function () {516let snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);517518assert.strictEqual(snippet.placeholders.length, 3);519let [first, second] = snippet.placeholders;520assert.strictEqual(first.index, 1);521assert.strictEqual(second.index, 2);522assert.ok(second.parent === first);523assert.ok(first.parent === snippet);524525snippet = new SnippetParser().parse('${VAR:default${1:value}}$0', true);526assert.strictEqual(snippet.placeholders.length, 2);527[first] = snippet.placeholders;528assert.strictEqual(first.index, 1);529530assert.ok(snippet.children[0] instanceof Variable);531assert.ok(first.parent === snippet.children[0]);532});533534test('TextmateSnippet#enclosingPlaceholders', () => {535const snippet = new SnippetParser().parse('This ${1:is ${2:nested}}$0', true);536const [first, second] = snippet.placeholders;537538assert.deepStrictEqual(snippet.enclosingPlaceholders(first), []);539assert.deepStrictEqual(snippet.enclosingPlaceholders(second), [first]);540});541542test('TextmateSnippet#offset', () => {543let snippet = new SnippetParser().parse('te$1xt', true);544assert.strictEqual(snippet.offset(snippet.children[0]), 0);545assert.strictEqual(snippet.offset(snippet.children[1]), 2);546assert.strictEqual(snippet.offset(snippet.children[2]), 2);547548snippet = new SnippetParser().parse('${TM_SELECTED_TEXT:def}', true);549assert.strictEqual(snippet.offset(snippet.children[0]), 0);550assert.strictEqual(snippet.offset((<Variable>snippet.children[0]).children[0]), 0);551552// forgein marker553assert.strictEqual(snippet.offset(new Text('foo')), -1);554});555556test('TextmateSnippet#placeholder', () => {557let snippet = new SnippetParser().parse('te$1xt$0', true);558let placeholders = snippet.placeholders;559assert.strictEqual(placeholders.length, 2);560561snippet = new SnippetParser().parse('te$1xt$1$0', true);562placeholders = snippet.placeholders;563assert.strictEqual(placeholders.length, 3);564565566snippet = new SnippetParser().parse('te$1xt$2$0', true);567placeholders = snippet.placeholders;568assert.strictEqual(placeholders.length, 3);569570snippet = new SnippetParser().parse('${1:bar${2:foo}bar}$0', true);571placeholders = snippet.placeholders;572assert.strictEqual(placeholders.length, 3);573});574575test('TextmateSnippet#replace 1/2', function () {576const snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);577578assert.strictEqual(snippet.placeholders.length, 3);579const [, second] = snippet.placeholders;580assert.strictEqual(second.index, 2);581582const enclosing = snippet.enclosingPlaceholders(second);583assert.strictEqual(enclosing.length, 1);584assert.strictEqual(enclosing[0].index, 1);585586const nested = new SnippetParser().parse('ddd$1eee$0', true);587snippet.replace(second, nested.children);588589assert.strictEqual(snippet.toString(), 'aaabbbdddeee');590assert.strictEqual(snippet.placeholders.length, 4);591assert.strictEqual(snippet.placeholders[0].index, 1);592assert.strictEqual(snippet.placeholders[1].index, 1);593assert.strictEqual(snippet.placeholders[2].index, 0);594assert.strictEqual(snippet.placeholders[3].index, 0);595596const newEnclosing = snippet.enclosingPlaceholders(snippet.placeholders[1]);597assert.ok(newEnclosing[0] === snippet.placeholders[0]);598assert.strictEqual(newEnclosing.length, 1);599assert.strictEqual(newEnclosing[0].index, 1);600});601602test('TextmateSnippet#replace 2/2', function () {603const snippet = new SnippetParser().parse('aaa${1:bbb${2:ccc}}$0', true);604605assert.strictEqual(snippet.placeholders.length, 3);606const [, second] = snippet.placeholders;607assert.strictEqual(second.index, 2);608609const nested = new SnippetParser().parse('dddeee$0', true);610snippet.replace(second, nested.children);611612assert.strictEqual(snippet.toString(), 'aaabbbdddeee');613assert.strictEqual(snippet.placeholders.length, 3);614});615616test('Snippet order for placeholders, #28185', function () {617618const _10 = new Placeholder(10);619const _2 = new Placeholder(2);620621assert.strictEqual(Placeholder.compareByIndex(_10, _2), 1);622});623624test('Maximum call stack size exceeded, #28983', function () {625new SnippetParser().parse('${1:${foo:${1}}}');626});627628test('Snippet can freeze the editor, #30407', function () {629630const seen = new Set<Marker>();631632seen.clear();633new SnippetParser().parse('class ${1:${TM_FILENAME/(?:\\A|_)([A-Za-z0-9]+)(?:\\.rb)?/(?2::\\u$1)/g}} < ${2:Application}Controller\n $3\nend').walk(marker => {634assert.ok(!seen.has(marker));635seen.add(marker);636return true;637});638639seen.clear();640new SnippetParser().parse('${1:${FOO:abc$1def}}').walk(marker => {641assert.ok(!seen.has(marker));642seen.add(marker);643return true;644});645});646647test('Snippets: make parser ignore `${0|choice|}`, #31599', function () {648assertTextAndMarker('${0|foo,bar|}', '${0|foo,bar|}', Text);649assertTextAndMarker('${1|foo,bar|}', 'foo', Placeholder);650});651652653test('Transform -> FormatString#resolve', function () {654655// shorthand functions656assert.strictEqual(new FormatString(1, 'upcase').resolve('foo'), 'FOO');657assert.strictEqual(new FormatString(1, 'downcase').resolve('FOO'), 'foo');658assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar'), 'Bar');659assert.strictEqual(new FormatString(1, 'capitalize').resolve('bar no repeat'), 'Bar no repeat');660assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-foo'), 'BarFoo');661assert.strictEqual(new FormatString(1, 'pascalcase').resolve('bar-42-foo'), 'Bar42Foo');662assert.strictEqual(new FormatString(1, 'pascalcase').resolve('snake_AndPascalCase'), 'SnakeAndPascalCase');663assert.strictEqual(new FormatString(1, 'pascalcase').resolve('kebab-AndPascalCase'), 'KebabAndPascalCase');664assert.strictEqual(new FormatString(1, 'pascalcase').resolve('_justPascalCase'), 'JustPascalCase');665assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-foo'), 'barFoo');666assert.strictEqual(new FormatString(1, 'camelcase').resolve('bar-42-foo'), 'bar42Foo');667assert.strictEqual(new FormatString(1, 'camelcase').resolve('snake_AndCamelCase'), 'snakeAndCamelCase');668assert.strictEqual(new FormatString(1, 'camelcase').resolve('kebab-AndCamelCase'), 'kebabAndCamelCase');669assert.strictEqual(new FormatString(1, 'camelcase').resolve('_JustCamelCase'), 'justCamelCase');670assert.strictEqual(new FormatString(1, 'notKnown').resolve('input'), 'input');671672// if673assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(undefined), '');674assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve(''), '');675assert.strictEqual(new FormatString(1, undefined, 'foo', undefined).resolve('bar'), 'foo');676677// else678assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(undefined), 'foo');679assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve(''), 'foo');680assert.strictEqual(new FormatString(1, undefined, undefined, 'foo').resolve('bar'), 'bar');681682// if-else683assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(undefined), 'foo');684assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve(''), 'foo');685assert.strictEqual(new FormatString(1, undefined, 'bar', 'foo').resolve('baz'), 'bar');686});687688test('Snippet variable transformation doesn\'t work if regex is complicated and snippet body contains \'$$\' #55627', function () {689const snippet = new SnippetParser().parse('const fileName = "${TM_FILENAME/(.*)\\..+$/$1/}"');690assert.strictEqual(snippet.toTextmateString(), 'const fileName = "${TM_FILENAME/(.*)\\..+$/${1}/}"');691});692693test('[BUG] HTML attribute suggestions: Snippet session does not have end-position set, #33147', function () {694695const { placeholders } = new SnippetParser().parse('src="$1"', true);696const [first, second] = placeholders;697698assert.strictEqual(placeholders.length, 2);699assert.strictEqual(first.index, 1);700assert.strictEqual(second.index, 0);701702});703704test('Snippet optional transforms are not applied correctly when reusing the same variable, #37702', function () {705706const transform = new Transform();707transform.appendChild(new FormatString(1, 'upcase'));708transform.appendChild(new FormatString(2, 'upcase'));709transform.regexp = /^(.)|-(.)/g;710711assert.strictEqual(transform.resolve('my-file-name'), 'MyFileName');712713const clone = transform.clone();714assert.strictEqual(clone.resolve('my-file-name'), 'MyFileName');715});716717test('problem with snippets regex #40570', function () {718719const snippet = new SnippetParser().parse('${TM_DIRECTORY/.*src[\\/](.*)/$1/}');720assertMarker(snippet, Variable);721});722723test('Variable transformation doesn\'t work if undefined variables are used in the same snippet #51769', function () {724const transform = new Transform();725transform.appendChild(new Text('bar'));726transform.regexp = new RegExp('foo', 'gi');727assert.strictEqual(transform.toTextmateString(), '/foo/bar/ig');728});729730test('Snippet parser freeze #53144', function () {731const snippet = new SnippetParser().parse('${1/(void$)|(.+)/${1:?-\treturn nil;}/}');732assertMarker(snippet, Placeholder);733});734735test('snippets variable not resolved in JSON proposal #52931', function () {736assertTextAndMarker('FOO${1:/bin/bash}', 'FOO/bin/bash', Text, Placeholder);737});738739test('Mirroring sequence of nested placeholders not selected properly on backjumping #58736', function () {740const snippet = new SnippetParser().parse('${3:nest1 ${1:nest2 ${2:nest3}}} $3');741assert.strictEqual(snippet.children.length, 3);742assert.ok(snippet.children[0] instanceof Placeholder);743assert.ok(snippet.children[1] instanceof Text);744assert.ok(snippet.children[2] instanceof Placeholder);745746function assertParent(marker: Marker) {747marker.children.forEach(assertParent);748if (!(marker instanceof Placeholder)) {749return;750}751let found = false;752let m: Marker = marker;753while (m && !found) {754if (m.parent === snippet) {755found = true;756}757m = m.parent;758}759assert.ok(found);760}761const [, , clone] = snippet.children;762assertParent(clone);763});764765test('Backspace can\'t be escaped in snippet variable transforms #65412', function () {766767const snippet = new SnippetParser().parse('namespace ${TM_DIRECTORY/[\\/]/\\\\/g};');768assertMarker(snippet, Text, Variable, Text);769});770771test('Snippet cannot escape closing bracket inside conditional insertion variable replacement #78883', function () {772773const snippet = new SnippetParser().parse('${TM_DIRECTORY/(.+)/${1:+import { hello \\} from world}/}');774const variable = <Variable>snippet.children[0];775assert.strictEqual(snippet.children.length, 1);776assert.ok(variable instanceof Variable);777assert.ok(variable.transform);778assert.strictEqual(variable.transform.children.length, 1);779assert.ok(variable.transform.children[0] instanceof FormatString);780assert.strictEqual((<FormatString>variable.transform.children[0]).ifValue, 'import { hello } from world');781assert.strictEqual((<FormatString>variable.transform.children[0]).elseValue, undefined);782});783784test('Snippet escape backslashes inside conditional insertion variable replacement #80394', function () {785786const snippet = new SnippetParser().parse('${CURRENT_YEAR/(.+)/${1:+\\\\}/}');787const variable = <Variable>snippet.children[0];788assert.strictEqual(snippet.children.length, 1);789assert.ok(variable instanceof Variable);790assert.ok(variable.transform);791assert.strictEqual(variable.transform.children.length, 1);792assert.ok(variable.transform.children[0] instanceof FormatString);793assert.strictEqual((<FormatString>variable.transform.children[0]).ifValue, '\\');794assert.strictEqual((<FormatString>variable.transform.children[0]).elseValue, undefined);795});796797test('Snippet placeholder empty right after expansion #152553', function () {798799const snippet = new SnippetParser().parse('${1:prog}: ${2:$1.cc} - $2');800const actual = snippet.toString();801assert.strictEqual(actual, 'prog: prog.cc - prog.cc');802803const snippet2 = new SnippetParser().parse('${1:prog}: ${3:${2:$1.cc}.33} - $2 $3');804const actual2 = snippet2.toString();805assert.strictEqual(actual2, 'prog: prog.cc.33 - prog.cc prog.cc.33');806807// cyclic references of placeholders808const snippet3 = new SnippetParser().parse('${1:$2.one} <> ${2:$1.two}');809const actual3 = snippet3.toString();810assert.strictEqual(actual3, '.two.one.two.one <> .one.two.one.two');811});812813test('Snippet choices are incorrectly escaped/applied #180132', function () {814assertTextAndMarker('${1|aaa$aaa|}bbb\\$bbb', 'aaa$aaabbb$bbb', Placeholder, Text);815});816});817818819