diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index cc245652..99e061f0 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -44,6 +44,8 @@ const doubleQuotesEscapeRegExp = /[\\]+"/g; const parentCompletionKind = CompletionItemKind.Class; +const existingProposeItem = '__'; + interface ParentCompletionItemOptions { schema: JSONSchema; indent?: string; @@ -59,6 +61,7 @@ interface CompletionsCollector { log(message: string): void; getNumberOfProposals(): number; result: CompletionList; + proposed: { [key: string]: CompletionItem }; } interface InsertText { @@ -181,7 +184,6 @@ export class YamlCompletion { } const proposed: { [key: string]: CompletionItem } = {}; - const existingProposeItem = '__'; const collector: CompletionsCollector = { add: (completionItem: CompletionItem, oneOfSchema: boolean) => { const addSuggestionForParent = function (completionItem: CompletionItem): void { @@ -288,6 +290,7 @@ export class YamlCompletion { return result.items.length; }, result, + proposed, }; if (this.customTags.length > 0) { @@ -996,6 +999,7 @@ export class YamlCompletion { indentFirstObject: false, shouldIndentWithTab: false, }, + [], 1 ); // add space before default snippet value @@ -1474,7 +1478,15 @@ export class YamlCompletion { }); value = fixedObj; } - insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings); + const existingProps = Object.keys(collector.proposed).filter( + (proposedProp) => collector.proposed[proposedProp].label === existingProposeItem + ); + insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, existingProps); + + // if snippet result is empty and value has a real value, don't add it as a completion + if (insertText === '' && value) { + continue; + } label = label || this.getLabelForSnippetValue(value); } else if (typeof s.bodyText === 'string') { let prefix = '', @@ -1503,10 +1515,15 @@ export class YamlCompletion { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, depth?: number): string { + private getInsertTextForSnippetValue( + value: unknown, + separatorAfter: string, + settings: StringifySettings, + existingProps: string[], + depth?: number + ): string { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const replacer = (value: any): string | any => { + const replacer = (value: unknown): string | any => { if (typeof value === 'string') { if (value[0] === '^') { return value.substr(1); @@ -1517,7 +1534,9 @@ export class YamlCompletion { } return value; }; - return stringifyObject(value, '', replacer, { ...settings, indentation: this.indentation }, depth) + separatorAfter; + return ( + stringifyObject(value, '', replacer, { ...settings, indentation: this.indentation, existingProps }, depth) + separatorAfter + ); } private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 872c5f26..00ef5483 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -11,6 +11,7 @@ export interface StringifySettings { interface StringifySettingsInternal extends StringifySettings { indentation: string; + existingProps: string[]; } export function stringifyObject( @@ -24,7 +25,7 @@ export function stringifyObject( if (obj !== null && typeof obj === 'object') { /** * When we are autocompleting a snippet from a property we need the indent so everything underneath the property - * is propertly indented. When we are auto completion from a value we don't want the indent because the cursor + * is properly indented. When we are auto completion from a value we don't want the indent because the cursor * is already in the correct place */ const newIndent = (depth === 0 && settings.shouldIndentWithTab) || depth > 0 ? indent + settings.indentation : ''; @@ -52,24 +53,32 @@ export function stringifyObject( return ''; } let result = (depth === 0 && settings.newLineFirst) || depth > 0 ? '\n' : ''; + let isFirstProp = true; for (let i = 0; i < keys.length; i++) { const key = keys[i]; + + if (depth === 0 && settings.existingProps.includes(key)) { + // Don't add existing properties to the YAML + continue; + } + const isObject = typeof obj[key] === 'object'; const colonDelimiter = isObject ? ':' : ': '; // add space only when value is primitive const parentArrayCompensation = isObject && /^\s|-/.test(key) ? settings.indentation : ''; // add extra space if parent is an array const objectIndent = newIndent + parentArrayCompensation; - // The first child of an array needs to be treated specially, otherwise identations will be off - if (depth === 0 && i === 0 && !settings.indentFirstObject) { - const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, (depth += 1), 0); - result += indent + key + colonDelimiter + value; + const lineBreak = isFirstProp ? '' : '\n'; // break line only if it's not the first property + + // The first child of an array needs to be treated specially, otherwise indentations will be off + if (depth === 0 && isFirstProp && !settings.indentFirstObject) { + const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, depth + 1, 0); + result += lineBreak + indent + key + colonDelimiter + value; } else { - const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, (depth += 1), 0); - result += newIndent + key + colonDelimiter + value; - } - if (i < keys.length - 1) { - result += '\n'; + const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, depth + 1, 0); + result += lineBreak + newIndent + key + colonDelimiter + value; } + + isFirstProp = false; } return result; } diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 4048b731..76fa569a 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -97,13 +97,22 @@ describe('Default Snippet Tests', () => { }) .then(done, done); }); - it('Snippet in array schema should autocomplete correctly inside array item ', (done) => { + it('Snippet in array schema should suggest nothing inside array item if YAML already contains all props', (done) => { const content = 'array:\n - item1: asd\n item2: asd\n '; const completion = parseSetup(content, content.length); + completion + .then(function (result) { + assert.equal(result.items.length, 0); + }) + .then(done, done); + }); + it('Snippet in array schema should suggest only some of the props inside an array item if YAML already contains some of the props', (done) => { + const content = 'array:\n - item1: asd\n '; + const completion = parseSetup(content, content.length); completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2'); + assert.equal(result.items[0].insertText, 'item2: $2'); assert.equal(result.items[0].label, 'My array item'); }) .then(done, done); @@ -167,6 +176,31 @@ describe('Default Snippet Tests', () => { .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 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].label, 'key'); + }) + .then(done, done); + }); + it('Snippet in object schema should not suggest snippet props because all of them are already in the YAML', (done) => { + const content = 'object:\n key:\n key1: value\n key2: value\n '; + const completion = parseSetup(content, content.length); + completion + .then(function (result) { + assert.equal(result.items.length, 1); + assert.equal(result.items[0].insertText, 'key:\n '); + assert.equal(result.items[0].label, 'key'); + }) + .then(done, done); + }); + it('Snippet in object schema should autocomplete on same line', (done) => { const content = 'object: '; // len: 9 const completion = parseSetup(content, 8);