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 });