From e7ad86793e718975953fd854c7bb43cd17a5b726 Mon Sep 17 00:00:00 2001
From: Petr Spacek
Date: Tue, 11 Mar 2025 12:27:18 +0100
Subject: [PATCH 1/2] feat: property snippets
---
.../services/yamlCompletion.ts | 97 +++-
src/languageservice/utils/json.ts | 2 +-
src/languageservice/utils/strings.ts | 15 +
test/autoCompletionFix.test.ts | 536 ++++++++++++++++++
test/defaultSnippets.test.ts | 8 +-
test/strings.test.ts | 83 ++-
6 files changed, 710 insertions(+), 31 deletions(-)
diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts
index 5283b76a0..4a525360a 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();
@@ -523,7 +524,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,
});
}
@@ -760,6 +761,7 @@ export class YamlCompletion {
key,
propertySchema,
separatorAfter,
+ collector,
identCompensation + this.indentation
);
}
@@ -787,6 +789,7 @@ export class YamlCompletion {
key,
propertySchema,
separatorAfter,
+ collector,
identCompensation + this.indentation
),
insertTextFormat: InsertTextFormat.Snippet,
@@ -944,7 +947,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 + ')' : '';
@@ -965,6 +968,7 @@ export class YamlCompletion {
key: string,
propertySchema: JSONSchema,
separatorAfter: string,
+ collector: CompletionsCollector,
indent = this.indentation
): string {
const propertyText = this.getInsertTextForValue(key, '', 'string');
@@ -1035,11 +1039,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) {
@@ -1079,10 +1083,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,
+ },
+ Object.keys(collector.proposed),
+ 0
+ );
+ value = addIndentationToMultilineString(value, indent, indent);
+
+ return { insertText: value, insertIndex };
+ }
+ }
if (!schema.properties) {
insertText = `${indent}$${insertIndex++}\n`;
return { insertText, insertIndex };
@@ -1122,18 +1146,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':
@@ -1141,6 +1169,7 @@ export class YamlCompletion {
const objectInsertResult = this.getInsertTextForObject(
propertySchema,
separatorAfter,
+ collector,
`${indent}${this.indentation}`,
insertIndex++
);
@@ -1176,8 +1205,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++}`;
@@ -1205,7 +1240,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;
}
@@ -1391,11 +1426,17 @@ export class YamlCompletion {
hasProposals = true;
});
}
- this.collectDefaultSnippets(schema, separatorAfter, collector, {
- newLineFirst: true,
- indentFirstObject: true,
- shouldIndentWithTab: true,
- });
+ this.collectDefaultSnippets(
+ schema,
+ separatorAfter,
+ collector,
+ {
+ newLineFirst: true,
+ indentFirstObject: true,
+ shouldIndentWithTab: true,
+ },
+ arrayDepth
+ );
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
}
@@ -1484,6 +1525,12 @@ export class YamlCompletion {
if (insertText === '' && value) {
continue;
}
+ // detection of specific situation: snippets for value Completion
+ // snippets located inside schema.items and line without hyphen (value completion)
+ if (arrayDepth === 1 && !Array.isArray(value) && settings.newLineFirst) {
+ insertText = addIndentationToMultilineString(insertText.trimStart(), `\n${this.indentation}- `, ' ');
+ }
+
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..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 d8ec5b040..bdf825038 100644
--- a/test/autoCompletionFix.test.ts
+++ b/test/autoCompletionFix.test.ts
@@ -1207,6 +1207,542 @@ objB:
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 - 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 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 - ',
+ },
+ ]);
+ });
+
+ // this is hard to fix, it requires bigger refactor of `collectDefaultSnippets` function
+ 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 (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 7ffaf9d8b..fbeff5c6e 100644
--- a/test/defaultSnippets.test.ts
+++ b/test/defaultSnippets.test.ts
@@ -163,7 +163,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);
@@ -177,7 +177,7 @@ 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);
@@ -191,7 +191,7 @@ describe('Default Snippet Tests', () => {
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: ');
assert.equal(result.items[1].label, 'key');
})
.then(done, done);
@@ -202,7 +202,7 @@ describe('Default Snippet Tests', () => {
completion
.then(function (result) {
assert.equal(result.items.length, 1);
- assert.equal(result.items[0].insertText, 'key:\n ');
+ assert.equal(result.items[0].insertText, 'key:\n');
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');
+ });
+ });
});
});
From d2d9bdd0a06f90b2fff4b6901185e44fe8928ce2 Mon Sep 17 00:00:00 2001
From: Petr Spacek
Date: Mon, 17 Mar 2025 11:17:44 +0100
Subject: [PATCH 2/2] fix: object property snippet - should not check existing
props in the yaml - because it's properties from the parent object
---
.../services/yamlCompletion.ts | 2 +-
test/autoCompletionFix.test.ts | 19 +++++++++++++++++++
test/defaultSnippets.test.ts | 7 ++++---
3 files changed, 24 insertions(+), 4 deletions(-)
diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts
index bcf4d4366..84fde7ea0 100644
--- a/src/languageservice/services/yamlCompletion.ts
+++ b/src/languageservice/services/yamlCompletion.ts
@@ -1118,7 +1118,7 @@ export class YamlCompletion {
indentFirstObject: false,
shouldIndentWithTab: false,
},
- Object.keys(collector.proposed),
+ [],
0
);
value = addIndentationToMultilineString(value, indent, indent);
diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts
index d0f9c3df3..af5efc871 100644
--- a/test/autoCompletionFix.test.ts
+++ b/test/autoCompletionFix.test.ts
@@ -1328,6 +1328,25 @@ snippets:
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`,
},
diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts
index 46abe0ddf..b38a4dae3 100644
--- a/test/defaultSnippets.test.ts
+++ b/test/defaultSnippets.test.ts
@@ -184,14 +184,14 @@ describe('Default Snippet Tests', () => {
});
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 key1: ');
+ 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);
@@ -202,7 +202,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);