Skip to content

Add transformations for placeholders #51621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/vs/editor/contrib/snippet/snippet.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ ${TM_FILENAME/(.*)\..+$/$1/}
|-> resolves to the filename
```

Placeholder-Transform
--

Like a Variable-Transform, a transformation of a placeholder allows changing the inserted text for the placeholder when moving to the next tab stop.
The inserted text is matched with the regular expression and the match or matches - depending on the options - are replaced with the specified replacement format text.
Every occurrence of a placeholder can define its own transformation independently using the value of the first placeholder.
The format for Placeholder-Transforms is the same as for Variable-Transforms.

The following sample removes an underscore at the beginning of the text. `_transform` becomes `transform`.

```
${1/^_(.*)/$1/}
| | | |-> No options
| | |
| | |-> Replace it with the first capture group
| |
| |-> Regular expression to capture everything after the underscore
|
|-> Placeholder Index
```

Grammar
--
Expand All @@ -61,12 +81,17 @@ Below is the EBNF for snippets. With `\` (backslash) you can escape `$`, `}` and

```
any ::= tabstop | placeholder | choice | variable | text
tabstop ::= '$' int | '${' int '}'
tabstop ::= '$' int
| '${' int '}'
| '${' int transform '}'
placeholder ::= '${' int ':' any '}'
| '${' int ':' any transform '}'
choice ::= '${' int '|' text (',' text)* '|}'
| '${' int '|' text (',' text)* '|' transform '}'
variable ::= '$' var | '${' var }'
| '${' var ':' any '}'
| '${' var '/' regex '/' (format | text)+ '/' options '}'
| '${' var transform '}'
transform ::= '/' regex '/' (format | text)+ '/' options
format ::= '$' int | '${' int '}'
| '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
| '${' int ':+' if '}'
Expand All @@ -78,3 +103,5 @@ var ::= [_a-zA-Z] [_a-zA-Z0-9]*
int ::= [0-9]+
text ::= .*
```

Transformations for placeholders and choices are an extension to the TextMate snippet grammar and only support by Visual Studio Code.
81 changes: 65 additions & 16 deletions src/vs/editor/contrib/snippet/snippetParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,11 @@ export class Text extends Marker {
}
}

export class Placeholder extends Marker {
export abstract class TransformableMarker extends Marker {
public transform: Transform;
}

export class Placeholder extends TransformableMarker {
static compareByIndex(a: Placeholder, b: Placeholder): number {
if (a.index === b.index) {
return 0;
Expand Down Expand Up @@ -247,17 +250,26 @@ export class Placeholder extends Marker {
}

toTextmateString(): string {
if (this.children.length === 0) {
let transformString = '';
if (this.transform) {
transformString = this.transform.toTextmateString();
}
if (this.children.length === 0 && !this.transform) {
return `\$${this.index}`;
} else if (this.children.length === 0) {
return `\${${this.index}${transformString}}`;
} else if (this.choice) {
return `\${${this.index}|${this.choice.toTextmateString()}|}`;
return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`;
} else {
return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}}`;
return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;
}
}

clone(): Placeholder {
let ret = new Placeholder(this.index);
if (this.transform) {
ret.transform = this.transform.clone();
}
ret._children = this.children.map(child => child.clone());
return ret;
}
Expand Down Expand Up @@ -384,17 +396,16 @@ export class FormatString extends Marker {
}
}

export class Variable extends Marker {
export class Variable extends TransformableMarker {

constructor(public name: string) {
super();
}

resolve(resolver: VariableResolver): boolean {
let value = resolver.resolve(this);
let [firstChild] = this._children;
if (firstChild instanceof Transform && this._children.length === 1) {
value = firstChild.resolve(value || '');
if (this.transform) {
value = this.transform.resolve(value || '');
}
if (value !== undefined) {
this._children = [new Text(value)];
Expand All @@ -404,15 +415,22 @@ export class Variable extends Marker {
}

toTextmateString(): string {
let transformString = '';
if (this.transform) {
transformString = this.transform.toTextmateString();
}
if (this.children.length === 0) {
return `\${${this.name}}`;
return `\${${this.name}${transformString}}`;
} else {
return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}}`;
return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;
}
}

clone(): Variable {
const ret = new Variable(this.name);
if (this.transform) {
ret.transform = this.transform.clone();
}
ret._children = this.children.map(child => child.clone());
return ret;
}
Expand Down Expand Up @@ -580,6 +598,7 @@ export class SnippetParser {
for (const placeholder of incompletePlaceholders) {
if (placeholderDefaultValues.has(placeholder.index)) {
const clone = new Placeholder(placeholder.index);
clone.transform = placeholder.transform;
for (const child of placeholderDefaultValues.get(placeholder.index)) {
clone.appendChild(child.clone());
}
Expand Down Expand Up @@ -696,6 +715,17 @@ export class SnippetParser {
return true;
}

//../<regex>/<format>/<options>} -> transform
if (this._accept(TokenType.Forwardslash)) {
if (this._parseTransform(placeholder)) {
parent.appendChild(placeholder);
return true;
}

this._backTo(token);
return false;
}

if (this._parse(placeholder)) {
continue;
}
Expand All @@ -717,18 +747,37 @@ export class SnippetParser {
continue;
}

if (this._accept(TokenType.Pipe) && this._accept(TokenType.CurlyClose)) {
// ..|} -> done
if (this._accept(TokenType.Pipe)) {
placeholder.appendChild(choice);
parent.appendChild(placeholder);
return true;
if (this._accept(TokenType.CurlyClose)) {
// ..|} -> done
parent.appendChild(placeholder);
return true;
}
if (this._accept(TokenType.Forwardslash)) {
// ...|/<regex>/<format>/<options>} -> transform
if (this._parseTransform(placeholder)) {
parent.appendChild(placeholder);
return true;
}
}
}
}

this._backTo(token);
return false;
}

} else if (this._accept(TokenType.Forwardslash)) {
// ${1/<regex>/<format>/<options>}
if (this._parseTransform(placeholder)) {
parent.appendChild(placeholder);
return true;
}

this._backTo(token);
return false;

} else if (this._accept(TokenType.CurlyClose)) {
// ${1}
parent.appendChild(placeholder);
Expand Down Expand Up @@ -829,7 +878,7 @@ export class SnippetParser {
}
}

private _parseTransform(parent: Variable): boolean {
private _parseTransform(parent: TransformableMarker): boolean {
// ...<regex>/<format>/<options>}

let transform = new Transform();
Expand Down Expand Up @@ -894,7 +943,7 @@ export class SnippetParser {
return false;
}

parent.appendChild(transform);
parent.transform = transform;
return true;
}

Expand Down
17 changes: 17 additions & 0 deletions src/vs/editor/contrib/snippet/snippetSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,23 @@ export class OneSnippet {

this._initDecorations();

// Transform placeholder text if necessary
if (this._placeholderGroupsIdx >= 0) {
let operations: IIdentifiedSingleEditOperation[] = [];

for (const placeholder of this._placeholderGroups[this._placeholderGroupsIdx]) {
// Check if the placeholder has a transformation
if (placeholder.transform) {
const id = this._placeholderDecorations.get(placeholder);
const range = this._editor.getModel().getDecorationRange(id);
const currentValue = this._editor.getModel().getValueInRange(range);

operations.push({ range: range, text: placeholder.transform.resolve(currentValue) });
}
}
this._editor.getModel().applyEdits(operations);
}

if (fwd === true && this._placeholderGroupsIdx < this._placeholderGroups.length - 1) {
this._placeholderGroupsIdx += 1;

Expand Down
90 changes: 90 additions & 0 deletions src/vs/editor/contrib/snippet/test/snippetParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,35 @@ suite('SnippetParser', () => {

});

test('Parser, placeholder transforms', function () {
assertTextAndMarker('${1///}', '', Placeholder);
assertTextAndMarker('${1/regex/format/gmi}', '', Placeholder);
assertTextAndMarker('${1/([A-Z][a-z])/format/}', '', Placeholder);

// tricky regex
assertTextAndMarker('${1/m\\/atch/$1/i}', '', Placeholder);
assertMarker('${1/regex\/format/options}', Text);

// incomplete
assertTextAndMarker('${1///', '${1///', Text);
assertTextAndMarker('${1/regex/format/options', '${1/regex/format/options', Text);
});

test('Parser, placeholder with defaults and transformation', () => {
assertTextAndMarker('${1:value/foo/bar/}', 'value', Placeholder);
assertTextAndMarker('${1:bar${2:foo}bar/foo/bar/}', 'barfoobar', Placeholder);

// incomplete
assertTextAndMarker('${1:bar${2:foobar}/foo/bar/', '${1:barfoobar/foo/bar/', Text, Placeholder, Text);
});

test('Parser, placeholder with choice and transformation', () => {
assertTextAndMarker('${1|one,two,three|/foo/bar/}', 'one', Placeholder);
assertTextAndMarker('${1|one|/foo/bar/}', 'one', Placeholder);
assertTextAndMarker('${1|one,two,three,|/foo/bar/}', '${1|one,two,three,|/foo/bar/}', Text);
assertTextAndMarker('${1|one,/foo/bar/', '${1|one,/foo/bar/', Text);
});

test('No way to escape forward slash in snippet regex #36715', function () {
assertMarker('${TM_DIRECTORY/src\\//$1/}', Variable);
});
Expand Down Expand Up @@ -378,6 +407,36 @@ suite('SnippetParser', () => {
assert.ok(marker[0] instanceof Variable);
});

test('Parser, transform example', () => {
let marker = new SnippetParser().parse('${1:name} : ${2:type}${3: :=/\\s:=(.*)/${1:+ :=}${1}/};\n$0');
let childs = marker.children;

assert.ok(childs[0] instanceof Placeholder);
assert.equal(childs[0].children.length, 1);
assert.equal(childs[0].children[0].toString(), 'name');
assert.equal((<Placeholder>childs[0]).transform, undefined);
assert.ok(childs[1] instanceof Text);
assert.equal(childs[1].toString(), ' : ');
assert.ok(childs[2] instanceof Placeholder);
assert.equal(childs[2].children.length, 1);
assert.equal(childs[2].children[0].toString(), 'type');
assert.ok(childs[3] instanceof Placeholder);
assert.equal(childs[3].children.length, 1);
assert.equal(childs[3].children[0].toString(), ' :=');
assert.notEqual((<Placeholder>childs[3]).transform, undefined);
let t = (<Placeholder>childs[3]).transform;
assert.equal(t.regexp, '/\\s:=(.*)/');
assert.equal(t.children.length, 2);
assert.ok(t.children[0] instanceof FormatString);
assert.equal((<FormatString>t.children[0]).index, 1);
assert.equal((<FormatString>t.children[0]).ifValue, ' :=');
assert.ok(t.children[1] instanceof FormatString);
assert.equal((<FormatString>t.children[1]).index, 1);
assert.ok(childs[4] instanceof Text);
assert.equal(childs[4].toString(), ';\n');

});

test('Parser, default placeholder values', () => {

assertMarker('errorContext: `${1:err}`, error: $1', Text, Placeholder, Text, Placeholder);
Expand All @@ -393,6 +452,37 @@ suite('SnippetParser', () => {
assert.equal((<Text>(<Placeholder>p2).children[0]), 'err');
});

test('Parser, default placeholder values and one transform', () => {

assertMarker('errorContext: `${1:err/err/ok/}`, error: $1', Text, Placeholder, Text, Placeholder);

const [, p1, , p2] = new SnippetParser().parse('errorContext: `${1:err/err/ok/}`, error:$1').children;

assert.equal((<Placeholder>p1).index, '1');
assert.equal((<Placeholder>p1).children.length, '1');
assert.equal((<Text>(<Placeholder>p1).children[0]), 'err');
assert.notEqual((<Placeholder>p1).transform, undefined);

assert.equal((<Placeholder>p2).index, '1');
assert.equal((<Placeholder>p2).children.length, '1');
assert.equal((<Text>(<Placeholder>p2).children[0]), 'err');
assert.equal((<Placeholder>p2).transform, undefined);

assertMarker('errorContext: `${1:err}`, error: ${1/err/ok/}', Text, Placeholder, Text, Placeholder);

const [, p3, , p4] = new SnippetParser().parse('errorContext: `${1:err}`, error:${1/err/ok/}').children;

assert.equal((<Placeholder>p3).index, '1');
assert.equal((<Placeholder>p3).children.length, '1');
assert.equal((<Text>(<Placeholder>p3).children[0]), 'err');
assert.equal((<Placeholder>p3).transform, undefined);

assert.equal((<Placeholder>p4).index, '1');
assert.equal((<Placeholder>p4).children.length, '1');
assert.equal((<Text>(<Placeholder>p4).children[0]), 'err');
assert.notEqual((<Placeholder>p4).transform, undefined);
});

test('Repeated snippet placeholder should always inherit, #31040', function () {
assertText('${1:foo}-abc-$1', 'foo-abc-foo');
assertText('${1:foo}-abc-${1}', 'foo-abc-foo');
Expand Down
Loading