From 25842337f00ee3a98411989d1db573016f81cdf8 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Thu, 23 Jan 2020 16:23:15 -0500 Subject: [PATCH] Fixed issue with arrays in default snippets --- .../services/yamlCompletion.ts | 29 +++++++--- src/languageservice/utils/json.ts | 58 ++++++++++++++----- test/defaultSnippets.test.ts | 34 +++++++++-- test/fixtures/defaultSnippets.json | 29 ++++++++++ 4 files changed, 119 insertions(+), 31 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 783b2f7f5..490c1f62c 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -232,13 +232,13 @@ export class YAMLCompletion extends JSONCompletion { const matchingSchemas = doc.getMatchingSchemas(schema.schema); matchingSchemas.forEach(s => { if (s.node === node && !s.inverted) { + this.collectDefaultSnippets(s.schema, separatorAfter, collector, { + newLineFirst: false, + indentFirstObject: false, + shouldIndentWithTab: false + }, false); const schemaProperties = s.schema.properties; if (schemaProperties) { - this.collectDefaultSnippets(s.schema, separatorAfter, collector, { - newLineFirst: false, - indentFirstObject: false, - shouldIndentWithTab: false - }, false); Object.keys(schemaProperties).forEach((key: string) => { const propertySchema = schemaProperties[key]; if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) { @@ -439,7 +439,7 @@ export class YAMLCompletion extends JSONCompletion { } } - private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean) { + private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean, arrayDepth = 0) { if (Array.isArray(schema.defaultSnippets)) { schema.defaultSnippets.forEach(s => { let type = schema.type; @@ -448,10 +448,21 @@ export class YAMLCompletion extends JSONCompletion { let insertText: string; let filterText: string; if (isDefined(value)) { + let type = schema.type; + for (let i = arrayDepth; i > 0; i--) { + value = [value]; + type = 'array'; + } insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, isArray); label = label || this.getLabelForSnippetValue(value); } else if (typeof s.bodyText === 'string') { let prefix = '', suffix = '', indent = ''; + for (let i = arrayDepth; i > 0; i--) { + prefix = prefix + indent + '[\n'; + suffix = suffix + '\n' + indent + ']'; + indent += '\t'; + type = 'array'; + } insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter; label = label || insertText; filterText = insertText.replace(/[\n]/g, ''); // remove new lines @@ -469,7 +480,7 @@ export class YAMLCompletion extends JSONCompletion { } // tslint:disable-next-line:no-any - private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean): string { + private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean, depth?: number): string { // tslint:disable-next-line:no-any const replacer = (value: any) => { if (typeof value === 'string') { @@ -492,7 +503,7 @@ export class YAMLCompletion extends JSONCompletion { }); value = fixedObj; } - return stringifyObject(value, '', replacer, settings) + separatorAfter; + return stringifyObject(value, '', replacer, settings, depth) + separatorAfter; } // tslint:disable-next-line:no-any @@ -669,7 +680,7 @@ export class YAMLCompletion extends JSONCompletion { newLineFirst: true, indentFirstObject: false, shouldIndentWithTab: false - }); + }, false, 1); } } nValueProposals += propertySchema.defaultSnippets.length; diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 24ca12962..f9a772288 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -10,25 +10,51 @@ export interface StringifySettings { shouldIndentWithTab: boolean; } -export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings): string { +// tslint:disable-next-line: no-any +export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings, depth = 0): string { if (obj !== null && typeof obj === 'object') { - const newIndent = settings.shouldIndentWithTab ? (indent + '\t') : indent; - const keys = Object.keys(obj); - if (keys.length === 0) { - return ''; - } - let result = settings.newLineFirst ? '\n' : ''; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - if (i === 0 && !settings.indentFirstObject) { - result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings); - } else { - result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings); + + /** + * When we are autocompleting a snippet from a property we need the indent so everything underneath the property + * is propertly indented. When we are auto completion from a value we don't want the indent because the cursor + * is already in the correct place + */ + let newIndent = ((depth === 0 && settings.shouldIndentWithTab) || depth > 0) ? (indent + ' ') : ''; + if (Array.isArray(obj)) { + if (obj.length === 0) { + return ''; + } + let result = ((depth === 0 && settings.newLineFirst) || depth > 0) ? '\n' : ''; + for (let i = 0; i < obj.length; i++) { + result += newIndent + stringifyObject(obj[i], indent, stringifyLiteral, settings, depth += 1); + if (i < obj.length - 1) { + result += '\n'; + } + } + result += indent; + return result; + } else { + let keys = Object.keys(obj); + if (keys.length === 0) { + return ''; + } + let result = ((depth === 0 && settings.newLineFirst) || depth > 0) ? '\n' : ''; + for (let i = 0; i < keys.length; i++) { + let key = keys[i]; + + // The first child of an array needs to be treated specially, otherwise identations will be off + if (depth === 0 && i === 0 && !settings.indentFirstObject) { + result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings, depth += 1); + } else { + result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings, depth += 1); + } + if (i < keys.length - 1) { + result += '\n'; + } } - result += '\n'; + result += indent; + return result; } - result += indent; - return result; } return stringifyLiteral(obj); } diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 6db1d22e1..9422e6e08 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -38,7 +38,7 @@ suite('Default Snippet Tests', () => { const completion = parseSetup(content, 11); completion.then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n'); + assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2'); assert.equal(result.items[0].label, 'My array item'); }).then(done, done); }); @@ -48,17 +48,17 @@ suite('Default Snippet Tests', () => { const completion = parseSetup(content, 24); completion.then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n'); + assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2'); assert.equal(result.items[0].label, 'My array item'); }).then(done, done); }); it('Snippet in array schema should autocomplete correctly after ', done => { - const content = 'array:\n - item1: asd\n - item2: asd\n '; + const content = 'array:\n - item1: asd\n item2: asd\n '; const completion = parseSetup(content, 40); completion.then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2\n'); + assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2'); assert.equal(result.items[0].label, 'My array item'); }).then(done, done); }); @@ -76,7 +76,7 @@ suite('Default Snippet Tests', () => { const completion = parseSetup(content, 11); completion.then(function (result) { assert.equal(result.items.length, 2); - assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n'); + assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2'); assert.equal(result.items[0].label, 'Object item'); assert.equal(result.items[1].insertText, 'key:\n\t$1'); assert.equal(result.items[1].label, 'key'); @@ -88,7 +88,7 @@ suite('Default Snippet Tests', () => { const completion = parseSetup(content, 20); completion.then(function (result) { assert.notEqual(result.items.length, 0); - assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n'); + assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2'); assert.equal(result.items[0].label, 'Object item'); assert.equal(result.items[1].insertText, 'key:\n\t$1'); assert.equal(result.items[1].label, 'key'); @@ -122,5 +122,27 @@ suite('Default Snippet Tests', () => { assert.equal(result.items[0].insertText, 'false'); }).then(done, done); }); + + it('Snippet in boolean schema should autocomplete on same line', done => { + const content = 'longSnippet: '; + const completion = parseSetup(content, 13); + completion.then(function (result) { + assert.equal(result.items.length, 1); + assert.equal(result.items[0].label, 'apply-manifests'); + // tslint:disable-next-line:max-line-length + 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 '); + }).then(done, done); + }); + + it('Snippet in boolean schema should autocomplete on same line', done => { + const content = 'lon '; + const completion = parseSetup(content, 3); + completion.then(function (result) { + assert.equal(result.items.length, 5); + assert.equal(result.items[4].label, 'longSnippet'); + // tslint:disable-next-line:max-line-length + 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 '); + }).then(done, done); + }); }); }); diff --git a/test/fixtures/defaultSnippets.json b/test/fixtures/defaultSnippets.json index d708ac3d2..0e29de4ec 100644 --- a/test/fixtures/defaultSnippets.json +++ b/test/fixtures/defaultSnippets.json @@ -50,6 +50,35 @@ "bodyText": "false" } ] + }, + "longSnippet": { + "type": "object", + "defaultSnippets": [ + { + "label": "apply-manifests", + "description": "Task", + "body": { + "name": "$1", + "taskRef": { + "name": "apply-manifests" + }, + "resources": { + "inputs": [ + { + "name": "source", + "resource": "$3" + } + ] + }, + "params": [ + { + "name": "manifest_dir", + "value": "$2" + } + ] + } + } + ] } } }