Skip to content

Commit 1dc7e48

Browse files
authored
Fix propertyNames keyword auto-completion (#1162)
* Fix propertyNames auto-completion Signed-off-by: Morgan Chang <[email protected]> * fix bugs in case allOf Signed-off-by: Morgan Chang <[email protected]> --------- Signed-off-by: Morgan Chang <[email protected]>
1 parent 8495b84 commit 1dc7e48

File tree

2 files changed

+159
-10
lines changed

2 files changed

+159
-10
lines changed

src/languageservice/services/yamlCompletion.ts

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -841,17 +841,36 @@ export class YamlCompletion {
841841
this.addSchemaValueCompletions(schema.schema, separatorAfter, collector, {}, ignoreScalars);
842842
}
843843

844-
if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === 'object') {
844+
if (schema.schema.type === 'object' && schema.schema.propertyNames && schema.schema.additionalProperties !== false) {
845845
const propertyNameSchema = asSchema(schema.schema.propertyNames);
846846
if (!propertyNameSchema.deprecationMessage && !propertyNameSchema.doNotSuggest) {
847-
const label = propertyNameSchema.title || 'property';
848-
collector.add({
849-
kind: CompletionItemKind.Property,
850-
label,
851-
insertText: '$' + `{1:${label}}: `,
852-
insertTextFormat: InsertTextFormat.Snippet,
853-
documentation: this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || '',
854-
});
847+
const doc = this.fromMarkup(
848+
(propertyNameSchema.markdownDescription || propertyNameSchema.description || '') +
849+
(propertyNameSchema.pattern ? `\n\n**Pattern:** \`${propertyNameSchema.pattern}\`` : '')
850+
);
851+
const { candidates, impossible } = this.getPropertyNamesCandidates(propertyNameSchema);
852+
if (impossible) {
853+
// suggest nothing
854+
} else if (candidates.length) {
855+
for (const key of candidates) {
856+
collector.add({
857+
kind: CompletionItemKind.Property,
858+
label: key,
859+
insertText: `${key}: `,
860+
insertTextFormat: InsertTextFormat.PlainText,
861+
documentation: doc,
862+
});
863+
}
864+
} else {
865+
const label = propertyNameSchema.title || 'property';
866+
collector.add({
867+
kind: CompletionItemKind.Property,
868+
label,
869+
insertText: '$' + `{1:${label}}: `,
870+
insertTextFormat: InsertTextFormat.Snippet,
871+
documentation: doc,
872+
});
873+
}
855874
}
856875
}
857876
}
@@ -1704,6 +1723,39 @@ export class YamlCompletion {
17041723
return 0;
17051724
}
17061725

1726+
private getPropertyNamesCandidates(schema: JSONSchemaRef): { candidates: string[]; impossible: boolean } {
1727+
let impossible = false;
1728+
1729+
const collect = (node: JSONSchemaRef): Set<string> | null => {
1730+
if (!node || typeof node !== 'object') return null;
1731+
1732+
if (Array.isArray(node.allOf) && node.allOf.length) {
1733+
let intersection = null;
1734+
for (const part of node.allOf) {
1735+
const partSet = collect(part);
1736+
if (!partSet) continue;
1737+
intersection = intersection ? new Set([...intersection].filter((v) => partSet.has(v))) : new Set(partSet);
1738+
if (intersection.size === 0) {
1739+
impossible = true;
1740+
return new Set();
1741+
}
1742+
}
1743+
if (intersection) return intersection;
1744+
}
1745+
1746+
const result = new Set<string>();
1747+
if (typeof node.const === 'string') result.add(node.const);
1748+
node.enum?.forEach((val) => typeof val === 'string' && result.add(val));
1749+
node.anyOf?.forEach((branch) => collect(branch)?.forEach((val) => result.add(val)));
1750+
node.oneOf?.forEach((branch) => collect(branch)?.forEach((val) => result.add(val)));
1751+
1752+
return result.size ? result : null;
1753+
};
1754+
1755+
const set = collect(schema);
1756+
return { candidates: set ? [...set] : [], impossible };
1757+
}
1758+
17071759
getQuote(): string {
17081760
return this.isSingleQuote ? `'` : `"`;
17091761
}

test/autoCompletionFix.test.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1478,7 +1478,7 @@ test1:
14781478

14791479
expect(completion.items.length).equal(1);
14801480
expect(completion.items[0].insertText).to.be.equal('${1:property}: ');
1481-
expect(completion.items[0].documentation).to.be.equal('Property Description');
1481+
expect(completion.items[0].documentation).to.be.deep.equal({ kind: 'markdown', value: 'Property Description' });
14821482
});
14831483
it('should not suggest propertyNames with doNotSuggest', async () => {
14841484
const schema: JSONSchema = {
@@ -1515,6 +1515,103 @@ test1:
15151515
expect(completion.items[1].insertText).to.be.equal('"NO"');
15161516
});
15171517

1518+
it('should suggest propertyNames keys from definitions $ref', async () => {
1519+
const schema: JSONSchema = {
1520+
definitions: {
1521+
EventName: {
1522+
type: 'string',
1523+
title: 'EventName',
1524+
enum: ['None', 'Event1', 'Event2'],
1525+
},
1526+
},
1527+
type: 'object',
1528+
properties: {
1529+
events: {
1530+
type: 'object',
1531+
propertyNames: { $ref: '#/definitions/EventName' },
1532+
},
1533+
},
1534+
required: ['events'],
1535+
};
1536+
schemaProvider.addSchema(SCHEMA_ID, schema);
1537+
const completion = await parseSetup('events:\n ', 1, 2);
1538+
expect(completion.items.map((i) => i.label)).to.have.members(['None', 'Event1', 'Event2']);
1539+
});
1540+
1541+
it('should suggest propertyNames candidates from const', async () => {
1542+
const schema: JSONSchema = {
1543+
type: 'object',
1544+
propertyNames: {
1545+
const: 'Event0',
1546+
},
1547+
};
1548+
schemaProvider.addSchema(SCHEMA_ID, schema);
1549+
const completion = await parseSetup('', 0, 0);
1550+
expect(completion.items.map((i) => i.label)).to.have.members(['Event0']);
1551+
});
1552+
1553+
it('should suggest propertyNames candidates from enum', async () => {
1554+
const schema: JSONSchema = {
1555+
type: 'object',
1556+
additionalProperties: true,
1557+
propertyNames: {
1558+
enum: ['Event1', 'Event2', 'Event3'],
1559+
},
1560+
};
1561+
schemaProvider.addSchema(SCHEMA_ID, schema);
1562+
const completion = await parseSetup('', 0, 0);
1563+
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2', 'Event3']);
1564+
});
1565+
1566+
it('should suggest propertyNames candidates from oneOf', async () => {
1567+
const schema: JSONSchema = {
1568+
type: 'object',
1569+
propertyNames: {
1570+
oneOf: [{ const: 'Event1' }, { const: 'Event2' }],
1571+
},
1572+
};
1573+
schemaProvider.addSchema(SCHEMA_ID, schema);
1574+
const completion = await parseSetup('', 0, 0);
1575+
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2']);
1576+
});
1577+
1578+
it('should suggest propertyNames candidates from anyOf', async () => {
1579+
const schema: JSONSchema = {
1580+
type: 'object',
1581+
propertyNames: {
1582+
anyOf: [{ const: 'Event1' }, { enum: ['Event2', 'Event3'] }],
1583+
},
1584+
};
1585+
schemaProvider.addSchema(SCHEMA_ID, schema);
1586+
const completion = await parseSetup('', 0, 0);
1587+
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2', 'Event3']);
1588+
});
1589+
1590+
it('should suggest only the intersected propertyNames candidate from allOf (const + enum)', async () => {
1591+
const schema: JSONSchema = {
1592+
type: 'object',
1593+
propertyNames: {
1594+
allOf: [{ const: 'One' }, { enum: ['One', 'Two'] }],
1595+
},
1596+
};
1597+
schemaProvider.addSchema(SCHEMA_ID, schema);
1598+
const completion = await parseSetup('', 0, 0);
1599+
expect(completion.items.map((i) => i.label)).to.have.members(['One']);
1600+
expect(completion.items.map((i) => i.label)).to.not.include('Two');
1601+
});
1602+
1603+
it('should not suggest any propertyNames when allOf makes keys impossible (const + const)', async () => {
1604+
const schema: JSONSchema = {
1605+
type: 'object',
1606+
propertyNames: {
1607+
allOf: [{ const: 'One' }, { const: 'Two' }],
1608+
},
1609+
};
1610+
schemaProvider.addSchema(SCHEMA_ID, schema);
1611+
const completion = await parseSetup('', 0, 0);
1612+
expect(completion.items).to.be.empty;
1613+
});
1614+
15181615
describe('String scalar completion comprehensive tests', () => {
15191616
const STRING_CASES: {
15201617
value: string;

0 commit comments

Comments
 (0)