Skip to content

Commit 2584233

Browse files
committed
Fixed issue with arrays in default snippets
1 parent d613734 commit 2584233

4 files changed

Lines changed: 119 additions & 31 deletions

File tree

src/languageservice/services/yamlCompletion.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,13 @@ export class YAMLCompletion extends JSONCompletion {
232232
const matchingSchemas = doc.getMatchingSchemas(schema.schema);
233233
matchingSchemas.forEach(s => {
234234
if (s.node === node && !s.inverted) {
235+
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
236+
newLineFirst: false,
237+
indentFirstObject: false,
238+
shouldIndentWithTab: false
239+
}, false);
235240
const schemaProperties = s.schema.properties;
236241
if (schemaProperties) {
237-
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
238-
newLineFirst: false,
239-
indentFirstObject: false,
240-
shouldIndentWithTab: false
241-
}, false);
242242
Object.keys(schemaProperties).forEach((key: string) => {
243243
const propertySchema = schemaProperties[key];
244244
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) {
@@ -439,7 +439,7 @@ export class YAMLCompletion extends JSONCompletion {
439439
}
440440
}
441441

442-
private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean) {
442+
private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean, arrayDepth = 0) {
443443
if (Array.isArray(schema.defaultSnippets)) {
444444
schema.defaultSnippets.forEach(s => {
445445
let type = schema.type;
@@ -448,10 +448,21 @@ export class YAMLCompletion extends JSONCompletion {
448448
let insertText: string;
449449
let filterText: string;
450450
if (isDefined(value)) {
451+
let type = schema.type;
452+
for (let i = arrayDepth; i > 0; i--) {
453+
value = [value];
454+
type = 'array';
455+
}
451456
insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, isArray);
452457
label = label || this.getLabelForSnippetValue(value);
453458
} else if (typeof s.bodyText === 'string') {
454459
let prefix = '', suffix = '', indent = '';
460+
for (let i = arrayDepth; i > 0; i--) {
461+
prefix = prefix + indent + '[\n';
462+
suffix = suffix + '\n' + indent + ']';
463+
indent += '\t';
464+
type = 'array';
465+
}
455466
insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter;
456467
label = label || insertText;
457468
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
@@ -469,7 +480,7 @@ export class YAMLCompletion extends JSONCompletion {
469480
}
470481

471482
// tslint:disable-next-line:no-any
472-
private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean): string {
483+
private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean, depth?: number): string {
473484
// tslint:disable-next-line:no-any
474485
const replacer = (value: any) => {
475486
if (typeof value === 'string') {
@@ -492,7 +503,7 @@ export class YAMLCompletion extends JSONCompletion {
492503
});
493504
value = fixedObj;
494505
}
495-
return stringifyObject(value, '', replacer, settings) + separatorAfter;
506+
return stringifyObject(value, '', replacer, settings, depth) + separatorAfter;
496507
}
497508

498509
// tslint:disable-next-line:no-any
@@ -669,7 +680,7 @@ export class YAMLCompletion extends JSONCompletion {
669680
newLineFirst: true,
670681
indentFirstObject: false,
671682
shouldIndentWithTab: false
672-
});
683+
}, false, 1);
673684
}
674685
}
675686
nValueProposals += propertySchema.defaultSnippets.length;

src/languageservice/utils/json.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,51 @@ export interface StringifySettings {
1010
shouldIndentWithTab: boolean;
1111
}
1212

13-
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings): string {
13+
// tslint:disable-next-line: no-any
14+
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings, depth = 0): string {
1415
if (obj !== null && typeof obj === 'object') {
15-
const newIndent = settings.shouldIndentWithTab ? (indent + '\t') : indent;
16-
const keys = Object.keys(obj);
17-
if (keys.length === 0) {
18-
return '';
19-
}
20-
let result = settings.newLineFirst ? '\n' : '';
21-
for (let i = 0; i < keys.length; i++) {
22-
const key = keys[i];
23-
if (i === 0 && !settings.indentFirstObject) {
24-
result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
25-
} else {
26-
result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
16+
17+
/**
18+
* When we are autocompleting a snippet from a property we need the indent so everything underneath the property
19+
* is propertly indented. When we are auto completion from a value we don't want the indent because the cursor
20+
* is already in the correct place
21+
*/
22+
let newIndent = ((depth === 0 && settings.shouldIndentWithTab) || depth > 0) ? (indent + ' ') : '';
23+
if (Array.isArray(obj)) {
24+
if (obj.length === 0) {
25+
return '';
26+
}
27+
let result = ((depth === 0 && settings.newLineFirst) || depth > 0) ? '\n' : '';
28+
for (let i = 0; i < obj.length; i++) {
29+
result += newIndent + stringifyObject(obj[i], indent, stringifyLiteral, settings, depth += 1);
30+
if (i < obj.length - 1) {
31+
result += '\n';
32+
}
33+
}
34+
result += indent;
35+
return result;
36+
} else {
37+
let keys = Object.keys(obj);
38+
if (keys.length === 0) {
39+
return '';
40+
}
41+
let result = ((depth === 0 && settings.newLineFirst) || depth > 0) ? '\n' : '';
42+
for (let i = 0; i < keys.length; i++) {
43+
let key = keys[i];
44+
45+
// The first child of an array needs to be treated specially, otherwise identations will be off
46+
if (depth === 0 && i === 0 && !settings.indentFirstObject) {
47+
result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings, depth += 1);
48+
} else {
49+
result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings, depth += 1);
50+
}
51+
if (i < keys.length - 1) {
52+
result += '\n';
53+
}
2754
}
28-
result += '\n';
55+
result += indent;
56+
return result;
2957
}
30-
result += indent;
31-
return result;
3258
}
3359
return stringifyLiteral(obj);
3460
}

test/defaultSnippets.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ suite('Default Snippet Tests', () => {
3838
const completion = parseSetup(content, 11);
3939
completion.then(function (result) {
4040
assert.equal(result.items.length, 1);
41-
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
41+
assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2');
4242
assert.equal(result.items[0].label, 'My array item');
4343
}).then(done, done);
4444
});
@@ -48,17 +48,17 @@ suite('Default Snippet Tests', () => {
4848
const completion = parseSetup(content, 24);
4949
completion.then(function (result) {
5050
assert.equal(result.items.length, 1);
51-
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
51+
assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2');
5252
assert.equal(result.items[0].label, 'My array item');
5353
}).then(done, done);
5454
});
5555

5656
it('Snippet in array schema should autocomplete correctly after ', done => {
57-
const content = 'array:\n - item1: asd\n - item2: asd\n ';
57+
const content = 'array:\n - item1: asd\n item2: asd\n ';
5858
const completion = parseSetup(content, 40);
5959
completion.then(function (result) {
6060
assert.equal(result.items.length, 1);
61-
assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2\n');
61+
assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2');
6262
assert.equal(result.items[0].label, 'My array item');
6363
}).then(done, done);
6464
});
@@ -76,7 +76,7 @@ suite('Default Snippet Tests', () => {
7676
const completion = parseSetup(content, 11);
7777
completion.then(function (result) {
7878
assert.equal(result.items.length, 2);
79-
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
79+
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2');
8080
assert.equal(result.items[0].label, 'Object item');
8181
assert.equal(result.items[1].insertText, 'key:\n\t$1');
8282
assert.equal(result.items[1].label, 'key');
@@ -88,7 +88,7 @@ suite('Default Snippet Tests', () => {
8888
const completion = parseSetup(content, 20);
8989
completion.then(function (result) {
9090
assert.notEqual(result.items.length, 0);
91-
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
91+
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2');
9292
assert.equal(result.items[0].label, 'Object item');
9393
assert.equal(result.items[1].insertText, 'key:\n\t$1');
9494
assert.equal(result.items[1].label, 'key');
@@ -122,5 +122,27 @@ suite('Default Snippet Tests', () => {
122122
assert.equal(result.items[0].insertText, 'false');
123123
}).then(done, done);
124124
});
125+
126+
it('Snippet in boolean schema should autocomplete on same line', done => {
127+
const content = 'longSnippet: ';
128+
const completion = parseSetup(content, 13);
129+
completion.then(function (result) {
130+
assert.equal(result.items.length, 1);
131+
assert.equal(result.items[0].label, 'apply-manifests');
132+
// tslint:disable-next-line:max-line-length
133+
assert.equal(result.items[0].insertText, '\n name: $1\n taskRef: \n name: apply-manifests \n resources: \n inputs: \n \n name: source\n resource: $3 \n params: \n \n name: manifest_dir\n value: $2 ');
134+
}).then(done, done);
135+
});
136+
137+
it('Snippet in boolean schema should autocomplete on same line', done => {
138+
const content = 'lon ';
139+
const completion = parseSetup(content, 3);
140+
completion.then(function (result) {
141+
assert.equal(result.items.length, 5);
142+
assert.equal(result.items[4].label, 'longSnippet');
143+
// tslint:disable-next-line:max-line-length
144+
assert.equal(result.items[4].insertText, 'longSnippet:\n name: $1\n taskRef: \n name: apply-manifests \n resources: \n inputs: \n \n name: source\n resource: $3 \n params: \n \n name: manifest_dir\n value: $2 ');
145+
}).then(done, done);
146+
});
125147
});
126148
});

test/fixtures/defaultSnippets.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,35 @@
5050
"bodyText": "false"
5151
}
5252
]
53+
},
54+
"longSnippet": {
55+
"type": "object",
56+
"defaultSnippets": [
57+
{
58+
"label": "apply-manifests",
59+
"description": "Task",
60+
"body": {
61+
"name": "$1",
62+
"taskRef": {
63+
"name": "apply-manifests"
64+
},
65+
"resources": {
66+
"inputs": [
67+
{
68+
"name": "source",
69+
"resource": "$3"
70+
}
71+
]
72+
},
73+
"params": [
74+
{
75+
"name": "manifest_dir",
76+
"value": "$2"
77+
}
78+
]
79+
}
80+
}
81+
]
5382
}
5483
}
5584
}

0 commit comments

Comments
 (0)