diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index aa0a1fea5..3db93cc9a 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'; import { SettingsState } from '../../yamlSettings'; const localize = nls.loadMessageBundle(); @@ -63,6 +64,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 { @@ -311,6 +326,7 @@ export class YamlCompletion { }, result, proposed, + context: {}, }; if (this.customTags && this.customTags.length > 0) { @@ -515,6 +531,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 @@ -543,7 +563,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, }); } @@ -780,6 +800,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, identCompensation + this.indentation ); } @@ -807,6 +828,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, identCompensation + this.indentation ), insertTextFormat: InsertTextFormat.Snippet, @@ -834,7 +856,7 @@ export class YamlCompletion { collector, {}, 'property', - Array.isArray(nodeParent.items) + Array.isArray(nodeParent.items) && !isInArray ); } @@ -932,13 +954,8 @@ export class YamlCompletion { if (index < s.schema.items.length) { this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, 'value'); } - } else if ( - typeof s.schema.items === 'object' && - (s.schema.items.type === 'object' || isAnyOfAllOfOneOfType(s.schema.items)) - ) { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } else { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value'); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } } } @@ -971,7 +988,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 + ')' : ''; @@ -981,7 +998,7 @@ export class YamlCompletion { ); collector.add({ kind: this.getSuggestionKind(schema.type), - label: '- (array item) ' + (schemaType || index), + label: '- (array item) ' + ((schemaType || index) ?? ''), documentation: documentation, insertText: insertText, insertTextFormat: InsertTextFormat.Snippet, @@ -992,6 +1009,7 @@ export class YamlCompletion { key: string, propertySchema: JSONSchema, separatorAfter: string, + collector: CompletionsCollector, indent = this.indentation ): string { const propertyText = this.getInsertTextForValue(key, '', 'string'); @@ -1062,11 +1080,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) { @@ -1106,10 +1124,35 @@ 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 + ); + if (Array.isArray(body)) { + // hyphen will be added later, so remove it, indent is ok + value = value.replace(/^\n( *)- /, '$1'); + } else { + value = addIndentationToMultilineString(value, indent, indent); + } + + return { insertText: value, insertIndex }; + } + } if (!schema.properties) { insertText = `${indent}$${insertIndex++}\n`; return { insertText, insertIndex }; @@ -1149,18 +1192,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}${key}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + insertText += + `${indent}${key}:` + + addIndentationToMultilineString( + arrayInsertResult.insertText, + `\n${indent}${this.indentation}- `, + `${this.indentation} ` + ); } break; case 'object': @@ -1168,6 +1215,7 @@ export class YamlCompletion { const objectInsertResult = this.getInsertTextForObject( propertySchema, separatorAfter, + collector, `${indent}${this.indentation}`, insertIndex++ ); @@ -1203,8 +1251,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++}`; @@ -1232,7 +1286,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; } @@ -1332,7 +1386,7 @@ export class YamlCompletion { ): void { if (typeof schema === 'object') { this.addEnumValueCompletions(schema, separatorAfter, collector, isArray); - this.addDefaultValueCompletions(schema, separatorAfter, collector); + this.addDefaultValueCompletions(schema, separatorAfter, collector, 0, isArray); this.collectTypes(schema, types); if (isArray && completionType === 'value' && !isAnyOfAllOfOneOfType(schema)) { @@ -1376,7 +1430,8 @@ export class YamlCompletion { schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, - arrayDepth = 0 + arrayDepth = 0, + isArray?: boolean ): void { let hasProposals = false; if (isDefined(schema.default)) { @@ -1418,13 +1473,21 @@ export class YamlCompletion { hasProposals = true; }); } - this.collectDefaultSnippets(schema, separatorAfter, collector, { - newLineFirst: true, - indentFirstObject: true, - shouldIndentWithTab: true, - }); + + this.collectDefaultSnippets( + schema, + separatorAfter, + collector, + { + newLineFirst: !isArray && !collector.context.hasHyphen, + indentFirstObject: !isArray && !collector.context.hasHyphen, + shouldIndentWithTab: !isArray && !collector.context.hasHyphen, + }, + 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); } } @@ -1452,10 +1515,11 @@ export class YamlCompletion { } else if (schema.enumDescriptions && i < schema.enumDescriptions.length) { documentation = schema.enumDescriptions[i]; } + const insertText = (isArray ? '- ' : '') + this.getInsertTextForValue(enm, separatorAfter, schema.type); collector.add({ kind: this.getSuggestionKind(schema.type), label: this.getLabelForValue(enm), - insertText: this.getInsertTextForValue(enm, separatorAfter, schema.type), + insertText, insertTextFormat: InsertTextFormat.Snippet, documentation: documentation, }); @@ -1478,38 +1542,53 @@ export class YamlCompletion { separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, - arrayDepth = 0 + arrayDepth = 0, + isArray = false ): void { 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') { - // 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 ); + insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, existingProps); + if (collector.context.hasHyphen && Array.isArray(value)) { + // modify the array snippet if the line contains a hyphen + insertText = insertText.replace(/^\n( *)- /, '$1'); + } + // if snippet result is empty and value has a real value, don't add it as a completion if (insertText === '' && value) { continue; } + + // postprocess of the array snippet that needs special handling based on the position in the yaml + if ((arrayDepth === 0 && type === 'array') || isArray || Array.isArray(value)) { + // 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}` : ''; + const addIndent = collector.context.hasColon || addHyphen ? this.indentation : ''; + // add extra indent if new line and hyphen are added + const addExtraIndent = isArray && addNewLine && addHyphen ? this.indentation : ''; + + insertText = addIndentationToMultilineString( + insertText.trimStart(), + `${addNewLine}${addHyphen}`, + `${addExtraIndent}${addIndent}` + ); + } + label = label || this.getLabelForSnippetValue(value); } else if (typeof s.bodyText === 'string') { let prefix = '', diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 00ef5483d..cc0deebf0 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -34,17 +34,27 @@ export function stringifyObject( if (obj.length === 0) { return ''; } + // don't indent the first element of the primitive array + const newIndent = depth > 0 ? indent + settings.indentation : ''; 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; } if (!Array.isArray(obj[i])) { pseudoObj = prependToObject(obj[i], consecutiveArrays); } - result += stringifyObject(pseudoObj, indent, stringifyLiteral, settings, (depth += 1), consecutiveArrays); + result += stringifyObject( + pseudoObj, + indent, + stringifyLiteral, + // overwrite the settings for array, it's valid for object type - not array + { ...settings, newLineFirst: true, shouldIndentWithTab: false }, + depth, + consecutiveArrays + ); } return result; } else { @@ -57,7 +67,7 @@ export function stringifyObject( for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (depth === 0 && settings.existingProps.includes(key)) { + if (depth === 0 && settings.existingProps.includes(key.replace(/^[-\s]+/, ''))) { // Don't add existing properties to the YAML 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/autoCompletion.test.ts b/test/autoCompletion.test.ts index e0cae5c31..06d580ec9 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1488,6 +1488,39 @@ describe('Auto Completion Tests', () => { .then(done, done); }); + it('Array of enum autocomplete on 2nd position without `-` should auto add `-` and `- (array item)`', (done) => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + references: { + type: 'array', + items: { + enum: ['Test'], + }, + }, + }, + }); + const content = 'references:\n - Test\n |\n|'; + const completion = parseSetup(content); + completion + .then(function (result) { + assert.deepEqual( + result.items.map((i) => ({ label: i.label, insertText: i.insertText })), + [ + { + insertText: '- Test', // auto added `- ` + label: 'Test', + }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); + }) + .then(done, done); + }); + it('Array of objects autocomplete with 4 space indentation check', async () => { const languageSettingsSetup = new ServiceSetup().withCompletion().withIndentation(' '); languageService.configure(languageSettingsSetup.languageSettings); @@ -1926,7 +1959,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 3, `Expecting 3 items in completion but found ${result.items.length}`); const resultDoc2 = await parseSetup(content, content.length); - assert.equal(resultDoc2.items.length, 0, `Expecting no items in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items.length, 1, `Expecting 1 item in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items[0].label, '- (array item) '); }); it('should handle absolute path', async () => { diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 0c83d36f1..1ec467414 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -570,6 +570,39 @@ objB: 'thing1:\n array2:\n - type: $1\n thing2:\n item1: $2\n item2: $3' ); }); + it('Autocomplete with snippet without hypen (-) inside an array', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + defaultSnippets: [ + { + label: 'My array item', + body: { item1: '$1' }, + }, + ], + required: ['thing1'], + properties: { + thing1: { + type: 'object', + required: ['item1'], + properties: { + item1: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }); + const content = 'array1:\n - thing1:\n item1: $1\n |\n|'; + const completion = await parseCaret(content); + + expect(completion.items[0].insertText).to.be.equal('- item1: '); + }); describe('array indent on different index position', () => { const schema = { type: 'object', diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 4066a2da6..3fc8c53f1 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -2,31 +2,74 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toFsPath, setupSchemaIDTextDocument, setupLanguageService, caretPosition } from './utils/testHelper'; import * as assert from 'assert'; +import { expect } from 'chai'; import * as path from 'path'; -import { ServiceSetup } from './utils/serviceSetup'; +import { JSONSchema } from 'vscode-json-languageservice'; +import { CompletionList, TextEdit } from 'vscode-languageserver-types'; import { LanguageHandlers } from '../src/languageserver/handlers/languageHandlers'; +import { LanguageService } from '../src/languageservice/yamlLanguageService'; import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings'; -import { CompletionList, TextEdit } from 'vscode-languageserver-types'; -import { expect } from 'chai'; +import { ServiceSetup } from './utils/serviceSetup'; +import { + caretPosition, + SCHEMA_ID, + setupLanguageService, + setupSchemaIDTextDocument, + TestCustomSchemaProvider, + toFsPath, +} from './utils/testHelper'; describe('Default Snippet Tests', () => { + let languageSettingsSetup: ServiceSetup; + let languageService: LanguageService; let languageHandler: LanguageHandlers; let yamlSettings: SettingsState; + let schemaProvider: TestCustomSchemaProvider; before(() => { const uri = toFsPath(path.join(__dirname, './fixtures/defaultSnippets.json')); const fileMatch = ['*.yml', '*.yaml']; - const languageSettingsSetup = new ServiceSetup().withCompletion().withSchemaFileMatch({ + languageSettingsSetup = new ServiceSetup().withCompletion().withSchemaFileMatch({ fileMatch, uri, }); - const { languageHandler: langHandler, yamlSettings: settings } = setupLanguageService(languageSettingsSetup.languageSettings); + const { + languageService: langService, + languageHandler: langHandler, + yamlSettings: settings, + schemaProvider: testSchemaProvider, + } = setupLanguageService(languageSettingsSetup.languageSettings); + languageService = langService; languageHandler = langHandler; yamlSettings = settings; + schemaProvider = testSchemaProvider; + }); + + afterEach(() => { + schemaProvider.deleteSchema(SCHEMA_ID); + languageService.configure(languageSettingsSetup.languageSettings); }); + /** + * Generates a completion list for the given document and caret (cursor) position. + * @param content The content of the document. + * The caret is located in the content using `|` bookends. + * For example, `content = 'ab|c|d'` places the caret over the `'c'`, at `position = 2` + * @returns A list of valid completions. + */ + function parseCaret(content: string): Promise { + const { position, content: content2 } = caretPosition(content); + + const testTextDocument = setupSchemaIDTextDocument(content2); + yamlSettings.documents = new TextDocumentTestManager(); + (yamlSettings.documents as TextDocumentTestManager).set(testTextDocument); + return languageHandler.completionHandler({ + position: testTextDocument.positionAt(position), + textDocument: testTextDocument, + }); + } + describe('Snippet Tests', function () { /** * Generates a completion list for the given document and caret (cursor) position. @@ -91,9 +134,16 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, '- item1: $1\n item2: $2'); - assert.equal(result.items[0].label, 'My array item'); + assert.deepEqual( + result.items.map((i) => ({ insertText: i.insertText, label: i.label })), + [ + { insertText: '- item1: $1\n item2: $2', label: 'My array item' }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); }) .then(done, done); }); @@ -156,7 +206,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); @@ -170,21 +220,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); @@ -195,7 +245,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); @@ -233,6 +284,19 @@ describe('Default Snippet Tests', () => { .then(done, done); }); + it('Snippet in string schema should autocomplete on same line (snippet is defined in body property)', (done) => { + const content = 'arrayStringValueSnippet:\n - |\n|'; + const completion = parseSetup(content); + completion + .then(function (result) { + assert.deepEqual( + result.items.map((i) => ({ label: i.label, insertText: i.insertText })), + [{ insertText: 'banana', label: 'Banana' }] + ); + }) + .then(done, done); + }); + it('Snippet in boolean schema should autocomplete on same line', (done) => { const content = 'boolean: | |'; // len: 10, pos: 9 const completion = parseSetup(content); @@ -266,7 +330,7 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content); completion .then(function (result) { - assert.equal(result.items.length, 15); // This is just checking the total number of snippets in the defaultSnippets.json + assert.equal(result.items.length, 16); // This is just checking the total number of snippets in the defaultSnippets.json assert.equal(result.items[4].label, 'longSnippet'); // eslint-disable-next-line assert.equal( @@ -342,9 +406,8 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 2); + assert.equal(result.items.length, 1); assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2'); - assert.equal(result.items[1].insertText, '\n item1: $1\n item2: $2'); }) .then(done, done); }); @@ -420,4 +483,938 @@ describe('Default Snippet Tests', () => { expect(item.textEdit.newText).to.be.equal('name: some'); }); }); + + 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(snippetObject) 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(snippetObject) 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(snippetObject) 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(snippetObject) 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`, + }, + ]); + }); + + it('should suggest defaultSnippet(snippetObject) 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(snippetObject) 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(snippetObject) 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(snippetObject) 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(snippetArray) on the property level as an object value', () => { + const schema = getNestedSchema({ + snippetArray: { + type: 'array', + items: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + }, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet(snippetArray) 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(snippetArray) 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(snippetArray) 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(snippetArray) 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(snippetArray) 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`, + }, + ]); + }); + + it('should suggest defaultSnippet(snippetArray) 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: 'labelSnippetArray', + insertText: `- item1: value + item2: value2`, + }, + { + label: '- (array item) object', + insertText: '- ', + }, + ]); + }); + }); + describe('defaultSnippets(snippetArray2) 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(snippetArray2) 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(snippetArray2) 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(snippetArray2) 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(snippetArray2) 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`, + ]); + }); + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - item1: test + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- item1: value\n item2: value2', + label: 'labelSnippetArray', + }, + { + insertText: '- item1: value\n item2: value2', + label: '- (array item) object', + }, + ]); + }); + }); // ARRAY - Snippet on items level + + describe('defaultSnippets(snippetArrayPrimitives) 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], + }, + ], + }, + }, + }); + + // implement if needed + // schema type array doesn't use defaultSnippets as a replacement for the auto generated result + // to change this, just return snippet result in `getInsertTextForProperty` function + + // it('should suggest defaultSnippet(snippetArrayPrimitives) 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(snippetArrayPrimitives) 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']); + }); + + it('should suggest defaultSnippet(snippetArrayPrimitives) 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(snippetArrayPrimitives) 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(snippetArray2Objects) outside items level, ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetArray2Objects: { + type: 'array', + items: { + type: 'object', + }, + defaultSnippets: [ + { + label: 'snippetArray2Objects', + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }); + + // schema type array doesn't use defaultSnippets as a replacement for the auto generated result + // to change this, just return snippet result in `getInsertTextForProperty` function + // it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - unfinished property', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArray2Objects|\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + // 'snippetArray2Objects:\n - item1: value\n item2: value2\n - item3: value', + // ]); + // }); + + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2Objects: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value with indent (without hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['- item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- item1: value\n item2: value2\n- item3: value', + label: 'snippetArray2Objects', + }, + { + insertText: '- $1\n', + label: '- (array item) object', + }, + ]); + }); + }); // ARRAY outside items - Body is array of objects + + describe('defaultSnippets(snippetArrayObjects) on the items level, ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetArrayObjects: { + type: 'array', + items: { + type: 'object', + defaultSnippets: [ + { + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects|\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetArrayObjects:\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayObjects: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '- item1: value\n item2: value2\n- item3: value', // array item snippet from getInsertTextForObject + '- item1: value\n item2: value2\n- item3: value', // from collectDefaultSnippets + ]); + }); + }); // ARRAY - Body is array of objects + + describe('defaultSnippets(snippetArrayString) on the items level, ARRAY - Body is string', () => { + const schema = getNestedSchema({ + snippetArrayString: { + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + body: 'value', + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet(snippetArrayString) 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(snippetArrayString) 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(snippetArrayString) 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(snippetArrayString) 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']); + }); + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - some other value + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- value', + label: '"value"', + }, + { + insertText: '- value', + label: '- (array item) string', + }, + ]); + }); + }); // ARRAY - Body is simple string + }); // ARRAY + + describe('anyOf(snippetAnyOfArray), ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetAnyOfArray: { + anyOf: [ + { + items: { + type: 'object', + }, + }, + { + type: 'object', + }, + ], + + defaultSnippets: [ + { + label: 'labelSnippetAnyOfArray', + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray|\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetAnyOfArray:\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAnyOfArray: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- $1\n', // could be better to suggest snippet - todo + label: '- (array item) object', + }, + ]); + }); + }); // anyOf - Body is array of objects + }); // variations of defaultSnippets }); diff --git a/test/fixtures/defaultSnippets.json b/test/fixtures/defaultSnippets.json index 5d4b69d2a..6b964a1e8 100644 --- a/test/fixtures/defaultSnippets.json +++ b/test/fixtures/defaultSnippets.json @@ -110,6 +110,18 @@ } ] }, + "arrayStringValueSnippet": { + "type": "array", + "items": { + "type": "string", + "defaultSnippets": [ + { + "label": "Banana", + "body": "banana" + } + ] + } + }, "arrayObjectSnippet": { "type": "object", "defaultSnippets": [ @@ -226,7 +238,7 @@ "body": { "item1": "$1", "item2": "$2" } } ], - "type": "string" + "type": "object" } } } 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'); + }); + }); }); });