diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 5466acb34..51cdc1c03 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -37,6 +37,7 @@ import { indexOf, isInComment, isMapContainsEmptyPair } from '../utils/astUtils' import { isModeline } from './modelineUtil'; import { getSchemaTypeName, isAnyOfAllOfOneOfType, isPrimitiveType } from '../utils/schemaUtils'; import { YamlNode } from '../jsonASTTypes'; +import { addIndentationToMultilineString } from '../utils/strings'; const localize = nls.loadMessageBundle(); @@ -62,6 +63,20 @@ interface CompletionsCollector { getNumberOfProposals(): number; result: CompletionList; proposed: { [key: string]: CompletionItem }; + context: { + /** + * The content of the line where the completion is happening. + */ + lineContent?: string; + /** + * `true` if the line has a colon. + */ + hasColon?: boolean; + /** + * `true` if the line starts with a hyphen. + */ + hasHyphen?: boolean; + }; } interface InsertText { @@ -459,6 +474,7 @@ export class YamlCompletion { }, result, proposed, + context: {}, }; if (this.customTags && this.customTags.length > 0) { @@ -670,6 +686,10 @@ export class YamlCompletion { } } + collector.context.lineContent = lineContent; + collector.context.hasColon = lineContent.indexOf(':') !== -1; + collector.context.hasHyphen = lineContent.trimStart().indexOf('-') === 0; + // completion for object keys if (node && isMap(node)) { // don't suggest properties that are already present @@ -698,7 +718,7 @@ export class YamlCompletion { collector.add({ kind: CompletionItemKind.Property, label: currentWord, - insertText: this.getInsertTextForProperty(currentWord, null, ''), + insertText: this.getInsertTextForProperty(currentWord, null, '', collector), insertTextFormat: InsertTextFormat.Snippet, }); } @@ -940,6 +960,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, indentCompensation + this.indentation ); } @@ -970,6 +991,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, indentCompensation + this.indentation ), insertTextFormat: InsertTextFormat.Snippet, @@ -1127,7 +1149,7 @@ export class YamlCompletion { index?: number ): void { const schemaType = getSchemaTypeName(schema); - const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimLeft()}`; + const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter, collector).insertText.trimLeft()}`; //append insertText to documentation const schemaTypeTitle = schemaType ? ' type `' + schemaType + '`' : ''; const schemaDescription = schema.description ? ' (' + schema.description + ')' : ''; @@ -1148,6 +1170,7 @@ export class YamlCompletion { key: string, propertySchema: JSONSchema, separatorAfter: string, + collector: CompletionsCollector, indent = this.indentation ): string { const propertyText = this.getInsertTextForValue(key, '', 'string'); @@ -1218,11 +1241,11 @@ export class YamlCompletion { nValueProposals += propertySchema.examples.length; } if (propertySchema.properties) { - return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, indent).insertText}`; + return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, collector, indent).insertText}`; } else if (propertySchema.items) { - return `${resultText}\n${indent}- ${ - this.getInsertTextForArray(propertySchema.items, separatorAfter, 1, indent).insertText - }`; + let insertText = this.getInsertTextForArray(propertySchema.items, separatorAfter, collector, 1, indent).insertText; + insertText = resultText + addIndentationToMultilineString(insertText, `\n${indent}- `, ' '); + return insertText; } if (nValueProposals === 0) { switch (type) { @@ -1262,10 +1285,30 @@ export class YamlCompletion { private getInsertTextForObject( schema: JSONSchema, separatorAfter: string, + collector: CompletionsCollector, indent = this.indentation, insertIndex = 1 ): InsertText { let insertText = ''; + if (Array.isArray(schema.defaultSnippets) && schema.defaultSnippets.length === 1) { + const body = schema.defaultSnippets[0].body; + if (isDefined(body)) { + let value = this.getInsertTextForSnippetValue( + body, + '', + { + newLineFirst: false, + indentFirstObject: false, + shouldIndentWithTab: false, + }, + [], + 0 + ); + value = addIndentationToMultilineString(value, indent, indent); + + return { insertText: value, insertIndex }; + } + } if (!schema.properties) { insertText = `${indent}$${insertIndex++}\n`; return { insertText, insertIndex }; @@ -1306,18 +1349,22 @@ export class YamlCompletion { } case 'array': { - const arrayInsertResult = this.getInsertTextForArray(propertySchema.items, separatorAfter, insertIndex++, indent); - const arrayInsertLines = arrayInsertResult.insertText.split('\n'); - let arrayTemplate = arrayInsertResult.insertText; - if (arrayInsertLines.length > 1) { - for (let index = 1; index < arrayInsertLines.length; index++) { - const element = arrayInsertLines[index]; - arrayInsertLines[index] = ` ${element}`; - } - arrayTemplate = arrayInsertLines.join('\n'); - } + const arrayInsertResult = this.getInsertTextForArray( + propertySchema.items, + separatorAfter, + collector, + insertIndex++, + indent + ); + insertIndex = arrayInsertResult.insertIndex; - insertText += `${indent}${keyEscaped}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + insertText += + `${indent}${keyEscaped}:` + + addIndentationToMultilineString( + arrayInsertResult.insertText, + `\n${indent}${this.indentation}- `, + `${this.indentation} ` + ); } break; case 'object': @@ -1325,6 +1372,7 @@ export class YamlCompletion { const objectInsertResult = this.getInsertTextForObject( propertySchema, separatorAfter, + collector, `${indent}${this.indentation}`, insertIndex++ ); @@ -1360,8 +1408,14 @@ export class YamlCompletion { return { insertText, insertIndex }; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getInsertTextForArray(schema: any, separatorAfter: string, insertIndex = 1, indent = this.indentation): InsertText { + private getInsertTextForArray( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + separatorAfter: string, + collector: CompletionsCollector, + insertIndex = 1, + indent = this.indentation + ): InsertText { let insertText = ''; if (!schema) { insertText = `$${insertIndex++}`; @@ -1389,7 +1443,7 @@ export class YamlCompletion { break; case 'object': { - const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, `${indent} `, insertIndex++); + const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, collector, indent, insertIndex++); insertText = objectInsertResult.insertText.trimLeft(); insertIndex = objectInsertResult.insertIndex; } @@ -1591,11 +1645,11 @@ export class YamlCompletion { indentFirstObject: !isArray, shouldIndentWithTab: !isArray, }, - 0, + arrayDepth, isArray ); if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { - this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1, true); } } @@ -1656,24 +1710,13 @@ export class YamlCompletion { if (Array.isArray(schema.defaultSnippets)) { for (const s of schema.defaultSnippets) { let type = schema.type; - let value = s.body; + const value = s.body; let label = s.label; let insertText: string; let filterText: string; if (isDefined(value)) { const type = s.type || schema.type; - if ((arrayDepth === 0 && type === 'array') || isArray) { - // We know that a - isn't present yet so we need to add one - const fixedObj = {}; - Object.keys(value).forEach((val, index) => { - if (index === 0 && !val.startsWith('-')) { - fixedObj[`- ${val}`] = value[val]; - } else { - fixedObj[` ${val}`] = value[val]; - } - }); - value = fixedObj; - } + const existingProps = Object.keys(collector.proposed).filter( (proposedProp) => collector.proposed[proposedProp].label === existingProposeItem ); @@ -1683,6 +1726,24 @@ export class YamlCompletion { if (insertText === '' && value) { continue; } + + if ((arrayDepth === 0 && type === 'array') || isArray) { + // add extra hyphen if we are in array, but the hyphen is missing on current line + // but don't add it for array value because it's already there from getInsertTextForSnippetValue + const addHyphen = !collector.context.hasHyphen && !Array.isArray(value) ? '- ' : ''; + // add new line if the cursor is after the colon + const addNewLine = collector.context.hasColon ? `\n${this.indentation}` : ''; + // add extra indent if new line and hyphen are added + const addIndent = isArray && addNewLine && addHyphen ? this.indentation : ''; + // const addIndent = addHyphen && addNewLine ? this.indentation : ''; + + insertText = addIndentationToMultilineString( + insertText.trimStart(), + `${addNewLine}${addHyphen}`, + `${addIndent}${this.indentation}` + ); + } + label = label || this.getLabelForSnippetValue(value); } else if (typeof s.bodyText === 'string') { let prefix = '', diff --git a/src/languageservice/services/yamlHoverDetail.ts b/src/languageservice/services/yamlHoverDetail.ts index 85f7de7cf..527da3f14 100644 --- a/src/languageservice/services/yamlHoverDetail.ts +++ b/src/languageservice/services/yamlHoverDetail.ts @@ -44,7 +44,11 @@ export class YamlHoverDetail { private schema2Md = new Schema2Md(); propTableStyle: YamlHoverDetailPropTableStyle; - constructor(schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) { + // eslint-disable-next-line prettier/prettier + constructor( + schemaService: YAMLSchemaService, + private readonly telemetry: Telemetry + ) { // this.shouldHover = true; this.schemaService = schemaService; } diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 00ef5483d..be76eb511 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -37,7 +37,7 @@ export function stringifyObject( let result = ''; for (let i = 0; i < obj.length; i++) { let pseudoObj = obj[i]; - if (typeof obj[i] !== 'object') { + if (typeof obj[i] !== 'object' || obj[i] === null) { result += '\n' + newIndent + '- ' + stringifyLiteral(obj[i]); continue; } diff --git a/src/languageservice/utils/strings.ts b/src/languageservice/utils/strings.ts index 6171411df..b0810e507 100644 --- a/src/languageservice/utils/strings.ts +++ b/src/languageservice/utils/strings.ts @@ -87,3 +87,18 @@ export function getFirstNonWhitespaceCharacterAfterOffset(str: string, offset: n } return offset; } + +export function addIndentationToMultilineString(text: string, firstIndent: string, nextIndent: string): string { + let wasFirstLineIndented = false; + return text.replace(/^.*$/gm, (match) => { + if (!match) { + return match; + } + // Add fistIndent to first line or if the previous line was empty + if (!wasFirstLineIndented) { + wasFirstLineIndented = true; + return firstIndent + match; + } + return nextIndent + match; // Add indent to other lines + }); +} diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index af62c7a12..0e8c92d98 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -598,7 +598,7 @@ objB: }, }, }); - const content = 'array1:\n - thing1:\n item1: $1\n | |'; + const content = 'array1:\n - thing1:\n item1: $1\n |\n|'; const completion = await parseCaret(content); // expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.eq([ @@ -1528,6 +1528,585 @@ test: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetBody']); }); + describe('variations of defaultSnippets', () => { + const getNestedSchema = (schema: JSONSchema['properties']): JSONSchema => { + return { + type: 'object', + properties: { + snippets: { + type: 'object', + properties: { + ...schema, + }, + }, + }, + }; + }; + + // STRING + describe('defaultSnippet for string property', () => { + const schema = getNestedSchema({ + snippetString: { + type: 'string', + defaultSnippets: [ + { + label: 'labelSnippetString', + body: 'value', + }, + ], + }, + }); + + it('should suggest defaultSnippet for STRING property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetStr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetString: value']); + }); + + it('should suggest defaultSnippet for STRING property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // STRING + + // OBJECT + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + required: ['item1'], + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1: 'value', + item2: { + item3: 'value nested', + }, + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value + item2: + item3: value nested`, + }, + ]); + }); + it('should suggest defaultSnippet for OBJECT property - unfinished property, should keep all snippet properties', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + item1: value + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value + item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: ` + item1: value + item2: + item3: value nested`, + }, + { + label: 'item1', // key intellisense + insertText: '\n item1: ', + }, + { + label: 'object', // parent intellisense + insertText: '\n item1: ', + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value with indent', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: `item1: value +item2: + item3: value nested`, + }, + { + label: 'item1', // key intellisense + insertText: 'item1: ', + }, + { + label: 'object', // parent intellisense + insertText: 'item1: ', + }, + ]); + }); + + it('should suggest partial defaultSnippet for OBJECT property - subset of items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', + insertText: `item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest no defaultSnippet for OBJECT property - all items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + item2: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([]); + }); + }); // OBJECT + + // OBJECT - Snippet nested + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { + type: 'object', + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1_1: 'value', + item1_2: { + item1_2_1: 'value nested', + }, + }, + }, + ], + }, + }, + required: ['item1'], + }, + }); + + it('should suggest defaultSnippet for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: + item1_1: value + item1_2: + item1_2_1: value nested`, + }, + ]); + }); + }); // OBJECT - Snippet nested + + // ARRAY + describe('defaultSnippet for ARRAY property', () => { + describe('defaultSnippets on the property level as an object value', () => { + const schema = getNestedSchema({ + snippetArray: { + type: 'array', + $comment: + 'property - Not implemented, OK value, OK value nested, OK value nested with -, OK on 2nd index without or with -', + items: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + }, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property (not implemented)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetArray', + insertText: 'snippetArray:\n - ', + }, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: ` + - item1: value + item2: value2`, + }, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (without hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: `- item1: value + item2: value2`, + }, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + }); + describe('defaultSnippets on the items level as an object value', () => { + const schema = getNestedSchema({ + snippetArray2: { + type: 'array', + items: { + type: 'object', + additionalProperties: true, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetArray2:\n - item1: value\n item2: value2', + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + ` + - item1: value + item2: value2`, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + }); // ARRAY - Snippet on items level + + describe('defaultSnippets on the items level, ARRAY - Body is array of primitives', () => { + const schema = getNestedSchema({ + snippetArrayPrimitives: { + type: 'array', + items: { + type: ['string', 'boolean', 'number', 'null'], + defaultSnippets: [ + { + body: ['value', 5, null, false], + }, + ], + }, + }, + }); + + // fix if needed + // it('should suggest defaultSnippet for ARRAY property with primitives - unfinished property', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives|\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + // 'snippetArrayPrimitives:\n - value\n - 5\n - null\n - false', + // ]); + // }); + + it('should suggest defaultSnippet for ARRAY property with primitives - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayPrimitives: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value\n - 5\n - null\n - false']); + }); + + // skip, fix if needed + // it('should suggest defaultSnippet for ARRAY property with primitives - value with indent (with hyphen)', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives: + // - |\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n - 5\n - null\n - false']); + // }); + // it('should suggest defaultSnippet for ARRAY property with primitives - value on 2nd position', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives: + // - some other value + // - |\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['- value\n - 5\n - null\n - false']); + // }); + }); // ARRAY - Body is array of primitives + + describe('defaultSnippets on the items level, ARRAY - Body is string', () => { + const schema = getNestedSchema({ + snippetArrayString: { + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + body: 'value', + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property with string - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - ${1}']); + // better to suggest, fix if needed + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + it('should suggest defaultSnippet for ARRAY property with string - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - some other value + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // ARRAY - Body is simple string + }); // ARRAY + }); + describe('should suggest prop of the object (based on not completed prop name)', () => { const schema: JSONSchema = { definitions: { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index b2e853df6..74e24f17b 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -164,7 +164,7 @@ describe('Default Snippet Tests', () => { assert.equal(result.items.length, 2); 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 '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -178,21 +178,21 @@ describe('Default Snippet Tests', () => { assert.notEqual(result.items.length, 0); 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 '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); assert.equal(result.items[1].label, 'key'); }) .then(done, done); }); it('Snippet in object schema should suggest some of the snippet props because some of them are already in the YAML', (done) => { - const content = 'object:\n key:\n key2: value\n '; + const content = 'object:\n key:\n key2: value\n '; // position is nested in `key` const completion = parseSetup(content, content.length); completion .then(function (result) { assert.notEqual(result.items.length, 0); assert.equal(result.items[0].insertText, 'key1: '); assert.equal(result.items[0].label, 'Object item'); - assert.equal(result.items[1].insertText, 'key:\n '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); // recursive item (key inside key) assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -203,7 +203,8 @@ describe('Default Snippet Tests', () => { completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'key:\n '); + // snippet for nested `key` property + assert.equal(result.items[0].insertText, 'key:\n key1: $1\n key2: $2'); // recursive item (key inside key) assert.equal(result.items[0].label, 'key'); }) .then(done, done); diff --git a/test/strings.test.ts b/test/strings.test.ts index 45741f7e9..4b808200d 100644 --- a/test/strings.test.ts +++ b/test/strings.test.ts @@ -2,7 +2,13 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { startsWith, endsWith, convertSimple2RegExp, safeCreateUnicodeRegExp } from '../src/languageservice/utils/strings'; +import { + startsWith, + endsWith, + convertSimple2RegExp, + safeCreateUnicodeRegExp, + addIndentationToMultilineString, +} from '../src/languageservice/utils/strings'; import * as assert from 'assert'; import { expect } from 'chai'; @@ -106,5 +112,80 @@ describe('String Tests', () => { const result = safeCreateUnicodeRegExp('^[\\w\\-_]+$'); expect(result).is.not.undefined; }); + + describe('addIndentationToMultilineString', () => { + it('should add indentation to a single line string', () => { + const text = 'hello'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' hello'); + }); + + it('should add indentation to a multiline string', () => { + const text = 'hello\nworld'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' hello\n world'); + }); + + it('should not indent empty string', () => { + const text = ''; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ''); + }); + + it('should not indent string with only newlines', () => { + const text = '\n\n'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n\n'); + }); + it('should not indent empty lines', () => { + const text = '\ntest\n'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n test\n'); + }); + + it('should handle string with multiple lines', () => { + const text = 'line1\nline2\nline3'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' line1\n line2\n line3'); + }); + + it('should handle string with multiple lines and tabs', () => { + const text = 'line1\nline2\nline3'; + const firstIndent = '\t'; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' line1\n line2\n line3'); + }); + + it('should prepare text for array snippet', () => { + const text = `obj: + prop1: value1 + prop2: value2`; + const firstIndent = '\n- '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n- obj:\n prop1: value1\n prop2: value2'); + }); + }); }); });