diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 63eb00cd9..5b9ff30ef 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -19,7 +19,7 @@ import { LanguageSettings } from '../yamlLanguageService'; import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService'; import { JSONCompletion } from 'vscode-json-languageservice/lib/umd/services/jsonCompletion'; import { ClientCapabilities } from 'vscode-languageserver-protocol'; -import { stringifyObject } from '../utils/json'; +import { stringifyObject, StringifySettings } from '../utils/json'; const localize = nls.loadMessageBundle(); export class YAMLCompletion extends JSONCompletion { @@ -234,6 +234,11 @@ export class YAMLCompletion extends JSONCompletion { if (s.node === node && !s.inverted) { 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']) { @@ -267,6 +272,25 @@ export class YAMLCompletion extends JSONCompletion { this.addSchemaValueCompletions(s.schema, separatorAfter, collector, { }); } } + + // Covers the case when we are showing a snippet in an array + if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') { + // For some reason the first item in the array needs to be treated differently, otherwise + // the indentation will not be correct + if (node.properties.length === 1) { + this.collectDefaultSnippets(s.schema, separatorAfter, collector, { + newLineFirst: false, + indentFirstObject: false, + shouldIndentWithTab: true + }, false); + } else { + this.collectDefaultSnippets(s.schema, separatorAfter, collector, { + newLineFirst: false, + indentFirstObject: true, + shouldIndentWithTab: false + }, false); + } + } }); } @@ -405,6 +429,17 @@ export class YAMLCompletion extends JSONCompletion { hasProposals = true; }); } + this.collectDefaultSnippets(schema, separatorAfter, collector, { + newLineFirst: true, + indentFirstObject: true, + shouldIndentWithTab: true + }, schema.type === 'array'); + if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); + } + } + + private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean) { if (Array.isArray(schema.defaultSnippets)) { schema.defaultSnippets.forEach(s => { let type = schema.type; @@ -413,24 +448,13 @@ 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); + 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 + label = label || insertText; + filterText = insertText.replace(/[\n]/g, ''); // remove new lines } collector.add({ kind: this.getSuggestionKind(type), @@ -440,16 +464,12 @@ export class YAMLCompletion extends JSONCompletion { insertTextFormat: InsertTextFormat.Snippet, filterText }); - hasProposals = true; }); } - if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { - this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); - } } // tslint:disable-next-line:no-any - private getInsertTextForSnippetValue(value: any, separatorAfter: string): string { + private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean): string { // tslint:disable-next-line:no-any const replacer = (value: any) => { if (typeof value === 'string') { @@ -457,9 +477,20 @@ export class YAMLCompletion extends JSONCompletion { return value.substr(1); } } - return JSON.stringify(value); + return value; }; - return stringifyObject(value, '', replacer) + separatorAfter; + if (isArray && typeof value === 'object' && value !== null) { + const fixedObj = { }; + Object.keys(value).forEach((val, index) => { + if (index === 0 && !val.startsWith('-')) { + fixedObj[`- ${val}`] = value[val]; + } else { + fixedObj[` ${val}`] = value[val]; + } + }); + value = fixedObj; + } + return stringifyObject(value, '', replacer, settings) + separatorAfter; } // tslint:disable-next-line:no-any @@ -632,7 +663,11 @@ export class YAMLCompletion extends JSONCompletion { if (propertySchema.defaultSnippets.length === 1) { const body = propertySchema.defaultSnippets[0].body; if (isDefined(body)) { - value = this.getInsertTextForSnippetValue(body, ''); + value = this.getInsertTextForSnippetValue(body, '', { + newLineFirst: true, + indentFirstObject: false, + shouldIndentWithTab: false + }); } } nValueProposals += propertySchema.defaultSnippets.length; diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index acc102cd6..7b341ef38 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -3,42 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +/** + * THIS IS GOING TO BE THE ONES THATS USED WHEN YOU AUTOCOMPLETE FROM A SCALAR + */ // tslint:disable-next-line: no-any -export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string): string { - if (obj !== null && typeof obj === 'object') { - const newIndent = indent + '\t'; - if (Array.isArray(obj)) { - if (obj.length === 0) { - return '[]'; - } - let result = '[\n'; - for (let i = 0; i < obj.length; i++) { - result += newIndent + stringifyObject(obj[i], newIndent, stringifyLiteral); - if (i < obj.length - 1) { - result += ','; - } - result += '\n'; - } - result += indent + ']'; - return result; - } else { - const keys = Object.keys(obj); - if (keys.length === 0) { - return '{}'; - } - let result = '{\n'; - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; +export interface StringifySettings { + newLineFirst: boolean; + indentFirstObject: boolean; + shouldIndentWithTab: boolean; +} - result += newIndent + JSON.stringify(key) + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral); - if (i < keys.length - 1) { - result += ','; - } - result += '\n'; +export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings): 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); } - result += indent + '}'; - return result; + result += '\n'; } + result += indent; + return result; } return stringifyLiteral(obj); } diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts new file mode 100644 index 000000000..6db1d22e1 --- /dev/null +++ b/test/defaultSnippets.test.ts @@ -0,0 +1,126 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TextDocument } from 'vscode-languageserver'; +import { getLanguageService } from '../src/languageservice/yamlLanguageService'; +import { toFsPath, schemaRequestService, workspaceContext } from './utils/testHelper'; +import assert = require('assert'); +import path = require('path'); + +const languageService = getLanguageService(schemaRequestService, workspaceContext, [], null); + +const languageSettings = { + schemas: [], + completion: true +}; + +const uri = toFsPath(path.join(__dirname, './fixtures/defaultSnippets.json')); +const fileMatch = ['*.yml', '*.yaml']; +languageSettings.schemas.push({ uri, fileMatch: fileMatch }); +languageService.configure(languageSettings); + +suite('Default Snippet Tests', () => { + + describe('Snippet Tests', function () { + + function setup(content: string) { + return TextDocument.create('file://~/Desktop/vscode-k8s/test.yaml', 'yaml', 0, content); + } + + function parseSetup(content: string, position: number) { + const testTextDocument = setup(content); + return languageService.doComplete(testTextDocument, testTextDocument.positionAt(position), false); + } + + it('Snippet in array schema should autocomplete with -', done => { + const content = 'array:\n - '; + 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].label, 'My array item'); + }).then(done, done); + }); + + it('Snippet in array schema should autocomplete on next line with depth', done => { + const content = 'array:\n - item1:\n - '; + 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].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 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].label, 'My array item'); + }).then(done, done); + }); + + it('Snippet in array schema should autocomplete on same line as array', done => { + const content = 'array: '; + const completion = parseSetup(content, 7); + completion.then(function (result) { + assert.equal(result.items.length, 1); + }).then(done, done); + }); + + it('Snippet in object schema should autocomplete on next line ', done => { + const content = 'object:\n '; + 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].label, 'Object item'); + assert.equal(result.items[1].insertText, 'key:\n\t$1'); + assert.equal(result.items[1].label, 'key'); + }).then(done, done); + }); + + it('Snippet in object schema should autocomplete on next line with depth', done => { + const content = 'object:\n key:\n '; + 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].label, 'Object item'); + assert.equal(result.items[1].insertText, 'key:\n\t$1'); + assert.equal(result.items[1].label, 'key'); + }).then(done, done); + }); + + it('Snippet in object schema should autocomplete on same line', done => { + const content = 'object: '; + const completion = parseSetup(content, 8); + completion.then(function (result) { + assert.equal(result.items.length, 1); + }).then(done, done); + }); + + it('Snippet in string schema should autocomplete on same line', done => { + const content = 'string: '; + const completion = parseSetup(content, 8); + completion.then(function (result) { + assert.notEqual(result.items.length, 0); + assert.equal(result.items[0].insertText, 'test $1'); + assert.equal(result.items[0].label, 'My string item'); + }).then(done, done); + }); + + it('Snippet in boolean schema should autocomplete on same line', done => { + const content = 'boolean: '; + const completion = parseSetup(content, 9); + completion.then(function (result) { + assert.notEqual(result.items.length, 0); + assert.equal(result.items[0].label, 'My boolean item'); + assert.equal(result.items[0].insertText, 'false'); + }).then(done, done); + }); + }); + }); diff --git a/test/fixtures/defaultSnippets.json b/test/fixtures/defaultSnippets.json new file mode 100644 index 000000000..d708ac3d2 --- /dev/null +++ b/test/fixtures/defaultSnippets.json @@ -0,0 +1,55 @@ +{ + "type": "object", + "properties": { + "object": { + "type": "object", + "defaultSnippets": [ + { + "label": "Object item", + "description": "Binds a key to a command for a given state", + "body": { "key1": "$1", "key2": "$2" } + } + ], + "properties": { + "key": { + "$ref": "#/properties/object" + } + } + }, + "array": { + "type": "array", + "defaultSnippets": [ + { + "label": "My array item", + "body": { "item1": "$1", "item2": "$2" } + } + ], + "items": { + "item1": { + "$ref": "#/properties/array" + }, + "item2": { + "$ref": "#/properties/array" + } + } + }, + "string": { + "type": "string", + "defaultSnippets": [ + { + "label": "My string item", + "bodyText": "test $1" + } + ] + }, + "boolean": { + "type": "boolean", + "defaultSnippets": [ + { + "label": "My boolean item", + "bodyText": "false" + } + ] + } + } +} diff --git a/test/fixtures/snippets.yaml b/test/fixtures/snippets.yaml new file mode 100644 index 000000000..1d9185074 --- /dev/null +++ b/test/fixtures/snippets.yaml @@ -0,0 +1,5 @@ +array: + - item1: asd + item2: asd + + \ No newline at end of file