From fae39cd8a03a29ac2f1617003c16b9537b6909cd Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Thu, 2 May 2024 11:23:09 +0200 Subject: [PATCH 1/4] fix: default value with special chars with anyOf --- .../services/yamlCompletion.ts | 25 ++++++++++------- test/autoCompletion.test.ts | 28 +++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 513a23d63..25188cbe9 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1116,7 +1116,7 @@ export class YamlCompletion { case 'anyOf': { let value = propertySchema.default || propertySchema.const; if (value) { - if (type === 'string') { + if (type === 'string' || typeof value === 'string') { value = convertToStringValue(value); } insertText += `${indent}${key}: \${${insertIndex++}:${value}}\n`; @@ -1230,7 +1230,7 @@ export class YamlCompletion { case 'string': { let snippetValue = JSON.stringify(value); snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes - snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + snippetValue = getInsertTextForPlainText(snippetValue); // escape \ and } if (type === 'string') { snippetValue = convertToStringValue(snippetValue); } @@ -1243,10 +1243,6 @@ export class YamlCompletion { return this.getInsertTextForValue(value, separatorAfter, type); } - private getInsertTextForPlainText(text: string): string { - return text.replace(/[\\$}]/g, '\\$&'); // escape $, \ and } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any private getInsertTextForValue(value: any, separatorAfter: string, type: string | string[]): string { if (value === null) { @@ -1259,13 +1255,13 @@ export class YamlCompletion { } case 'number': case 'boolean': - return this.getInsertTextForPlainText(value + separatorAfter); + return getInsertTextForPlainText(value + separatorAfter); } type = Array.isArray(type) ? type[0] : type; if (type === 'string') { value = convertToStringValue(value); } - return this.getInsertTextForPlainText(value + separatorAfter); + return getInsertTextForPlainText(value + separatorAfter); } private getInsertTemplateForValue( @@ -1290,14 +1286,14 @@ export class YamlCompletion { if (typeof element === 'object') { valueTemplate = `${this.getInsertTemplateForValue(element, indent + this.indentation, navOrder, separatorAfter)}`; } else { - valueTemplate = ` \${${navOrder.index++}:${this.getInsertTextForPlainText(element + separatorAfter)}}\n`; + valueTemplate = ` \${${navOrder.index++}:${getInsertTextForPlainText(element + separatorAfter)}}\n`; } insertText += `${valueTemplate}`; } } return insertText; } - return this.getInsertTextForPlainText(value + separatorAfter); + return getInsertTextForPlainText(value + separatorAfter); } private addSchemaValueCompletions( @@ -1669,6 +1665,13 @@ export class YamlCompletion { } } +/** + * escape $, \ and } + */ +function getInsertTextForPlainText(text: string): string { + return text.replace(/[\\$}]/g, '\\$&'); // +} + const isNumberExp = /^\d+$/; function convertToStringValue(param: unknown): string { let value: string; @@ -1681,6 +1684,8 @@ function convertToStringValue(param: unknown): string { return value; } + value = getInsertTextForPlainText(value); // escape $, \ and } + if (value === 'true' || value === 'false' || value === 'null' || isNumberExp.test(value)) { return `"${value}"`; } diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index eba8fa6d0..173eb37b7 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -931,6 +931,34 @@ describe('Auto Completion Tests', () => { ); }); + it('Autocompletion should escape $ in defaultValue in anyOf', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + car: { + type: 'object', + required: ['engine'], + properties: { + engine: { + anyOf: [ + { + type: 'object', + }, + { + type: 'string', + }, + ], + default: 'type$1234', + }, + }, + }, + }, + }); + const content = ''; + const completion = await parseSetup(content, 0); + expect(completion.items.map((i) => i.insertText)).to.deep.equal(['car:\n engine: ${1:type\\$1234}']); + }); + it('Autocompletion should escape colon when indicating map', async () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', From 26ec17a4f3f8621809ae8569e32fb6028f1c63b0 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Thu, 2 May 2024 14:31:10 +0200 Subject: [PATCH 2/4] chore: accept null,0,emptyString in default or const property --- src/languageservice/services/yamlCompletion.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 25188cbe9..9b4542cbf 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1114,8 +1114,8 @@ export class YamlCompletion { case 'number': case 'integer': case 'anyOf': { - let value = propertySchema.default || propertySchema.const; - if (value) { + let value = propertySchema.default === undefined ? propertySchema.const : propertySchema.default; + if (isDefined(value)) { if (type === 'string' || typeof value === 'string') { value = convertToStringValue(value); } From 33ef09a24ed0430447aaa10940bc037e07e74e8c Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Thu, 24 Oct 2024 15:31:41 +0200 Subject: [PATCH 3/4] fix: special chars in property --- src/languageservice/services/yamlCompletion.ts | 5 ++++- test/autoCompletion.test.ts | 15 +++++++++++++++ test/schemaValidation.test.ts | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 9b4542cbf..18c6c2e4c 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1669,7 +1669,10 @@ export class YamlCompletion { * escape $, \ and } */ function getInsertTextForPlainText(text: string): string { - return text.replace(/[\\$}]/g, '\\$&'); // + return text.replace(/(\\?)([\\$}])/g, (match, escapeChar, specialChar) => { + // If it's already escaped (has a backslash before it), return it as is + return escapeChar ? match : `\\${specialChar}`; + }); } const isNumberExp = /^\d+$/; diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 173eb37b7..6d3b5c373 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -959,6 +959,21 @@ describe('Auto Completion Tests', () => { expect(completion.items.map((i) => i.insertText)).to.deep.equal(['car:\n engine: ${1:type\\$1234}']); }); + it('Autocompletion should escape $ in property', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + $prop$1: { + type: 'string', + }, + }, + required: ['$prop$1'], + }); + const content = ''; + const completion = await parseSetup(content, 0); + expect(completion.items.map((i) => i.insertText)).includes('\\$prop\\$1: '); + }); + it('Autocompletion should escape colon when indicating map', async () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index 71268db7b..caed1f08e 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -1289,7 +1289,7 @@ obj: 4, 18, DiagnosticSeverity.Error, - 'yaml-schema: Package', + 'yaml-schema: Composer Package', 'https://raw.githubusercontent.com/composer/composer/master/res/composer-schema.json' ) ); From 67b24b1d3ff25e0deec42c12fb2cb344fdc996f8 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Fri, 25 Oct 2024 14:04:42 +0200 Subject: [PATCH 4/4] fix: special chars in object completion --- .../services/yamlCompletion.ts | 20 +++++--- test/autoCompletion.test.ts | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 18c6c2e4c..b6b13245d 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -640,8 +640,9 @@ export class YamlCompletion { completionItem.textEdit.newText = completionItem.insertText; } // remove $x or use {$x:value} in documentation - const mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, ''); - + let mdText = insertText.replace(/\${[0-9]+[:|](.*)}/g, (s, arg) => arg).replace(/\$([0-9]+)/g, ''); + // unescape special chars for markdown, reverse operation to getInsertTextForPlainText + mdText = getOriginalTextFromEscaped(mdText); const originalDocumentation = completionItem.documentation ? [completionItem.documentation, '', '----', ''] : []; completionItem.documentation = { kind: MarkupKind.Markdown, @@ -1095,6 +1096,7 @@ export class YamlCompletion { Object.keys(schema.properties).forEach((key: string) => { const propertySchema = schema.properties[key] as JSONSchema; + const keyEscaped = getInsertTextForPlainText(key); let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; if (!type) { if (propertySchema.anyOf) { @@ -1119,9 +1121,9 @@ export class YamlCompletion { if (type === 'string' || typeof value === 'string') { value = convertToStringValue(value); } - insertText += `${indent}${key}: \${${insertIndex++}:${value}}\n`; + insertText += `${indent}${keyEscaped}: \${${insertIndex++}:${value}}\n`; } else { - insertText += `${indent}${key}: $${insertIndex++}\n`; + insertText += `${indent}${keyEscaped}: $${insertIndex++}\n`; } break; } @@ -1138,7 +1140,7 @@ export class YamlCompletion { arrayTemplate = arrayInsertLines.join('\n'); } insertIndex = arrayInsertResult.insertIndex; - insertText += `${indent}${key}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + insertText += `${indent}${keyEscaped}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; } break; case 'object': @@ -1150,7 +1152,7 @@ export class YamlCompletion { insertIndex++ ); insertIndex = objectInsertResult.insertIndex; - insertText += `${indent}${key}:\n${objectInsertResult.insertText}\n`; + insertText += `${indent}${keyEscaped}:\n${objectInsertResult.insertText}\n`; } break; } @@ -1165,7 +1167,7 @@ export class YamlCompletion { }: \${${insertIndex++}:${propertySchema.default}}\n`; break; case 'string': - insertText += `${indent}${key}: \${${insertIndex++}:${convertToStringValue(propertySchema.default)}}\n`; + insertText += `${indent}${keyEscaped}: \${${insertIndex++}:${convertToStringValue(propertySchema.default)}}\n`; break; case 'array': case 'object': @@ -1675,6 +1677,10 @@ function getInsertTextForPlainText(text: string): string { }); } +function getOriginalTextFromEscaped(text: string): string { + return text.replace(/\\([\\$}])/g, '$1'); +} + const isNumberExp = /^\d+$/; function convertToStringValue(param: unknown): string { let value: string; diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 6d3b5c373..b58d43f20 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -318,6 +318,27 @@ describe('Auto Completion Tests', () => { expect(result.items[0].insertText).equal('validation:\n \\"null\\": ${1:false}'); }); }); + it('Autocomplete key object with special chars', async () => { + schemaProvider.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + $validation: { + type: 'object', + additionalProperties: false, + properties: { + $prop$1: { + type: 'string', + default: '$value$1', + }, + }, + }, + }, + }); + const content = ''; // len: 0 + const result = await parseSetup(content, 0); + expect(result.items.length).equal(1); + expect(result.items[0].insertText).equals('\\$validation:\n \\$prop\\$1: ${1:\\$value\\$1}'); + }); it('Autocomplete on boolean value (with value content)', (done) => { schemaProvider.addSchema(SCHEMA_ID, { @@ -3168,6 +3189,33 @@ describe('Auto Completion Tests', () => { expect(result.items.map((i) => i.label)).to.have.members(['fruit', 'vegetable']); }); + it('Should escape insert text with special chars but do not escape it in documenation', async () => { + const schema = { + properties: { + $prop1: { + properties: { + $prop2: { + type: 'string', + }, + }, + required: ['$prop2'], + }, + }, + required: ['$prop1'], + }; + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ''; + const result = await parseSetup(content, content.length); + + expect( + result.items.map((i) => ({ inserText: i.insertText, documentation: (i.documentation as MarkupContent).value })) + ).to.deep.equal([ + { + inserText: '\\$prop1:\n \\$prop2: ', + documentation: '```yaml\n$prop1:\n $prop2: \n```', + }, + ]); + }); }); it('Should function when settings are undefined', async () => { languageService.configure({ completion: true });