Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 61 additions & 9 deletions src/languageservice/services/yamlCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,17 +841,36 @@ export class YamlCompletion {
this.addSchemaValueCompletions(schema.schema, separatorAfter, collector, {}, ignoreScalars);
}

if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === 'object') {
if (schema.schema.type === 'object' && schema.schema.propertyNames && schema.schema.additionalProperties !== false) {
const propertyNameSchema = asSchema(schema.schema.propertyNames);
if (!propertyNameSchema.deprecationMessage && !propertyNameSchema.doNotSuggest) {
const label = propertyNameSchema.title || 'property';
collector.add({
kind: CompletionItemKind.Property,
label,
insertText: '$' + `{1:${label}}: `,
insertTextFormat: InsertTextFormat.Snippet,
documentation: this.fromMarkup(propertyNameSchema.markdownDescription) || propertyNameSchema.description || '',
});
const doc = this.fromMarkup(
(propertyNameSchema.markdownDescription || propertyNameSchema.description || '') +
(propertyNameSchema.pattern ? `\n\n**Pattern:** \`${propertyNameSchema.pattern}\`` : '')
);
const { candidates, impossible } = this.getPropertyNamesCandidates(propertyNameSchema);
if (impossible) {
// suggest nothing
} else if (candidates.length) {
for (const key of candidates) {
collector.add({
kind: CompletionItemKind.Property,
label: key,
insertText: `${key}: `,
insertTextFormat: InsertTextFormat.PlainText,
documentation: doc,
});
}
} else {
const label = propertyNameSchema.title || 'property';
collector.add({
kind: CompletionItemKind.Property,
label,
insertText: '$' + `{1:${label}}: `,
insertTextFormat: InsertTextFormat.Snippet,
documentation: doc,
});
}
}
}
}
Expand Down Expand Up @@ -1704,6 +1723,39 @@ export class YamlCompletion {
return 0;
}

private getPropertyNamesCandidates(schema: JSONSchemaRef): { candidates: string[]; impossible: boolean } {
let impossible = false;

const collect = (node: JSONSchemaRef): Set<string> | null => {
if (!node || typeof node !== 'object') return null;

if (Array.isArray(node.allOf) && node.allOf.length) {
let intersection = null;
for (const part of node.allOf) {
const partSet = collect(part);
if (!partSet) continue;
intersection = intersection ? new Set([...intersection].filter((v) => partSet.has(v))) : new Set(partSet);
if (intersection.size === 0) {
impossible = true;
return new Set();
}
}
if (intersection) return intersection;
}

const result = new Set<string>();
if (typeof node.const === 'string') result.add(node.const);
node.enum?.forEach((val) => typeof val === 'string' && result.add(val));
node.anyOf?.forEach((branch) => collect(branch)?.forEach((val) => result.add(val)));
node.oneOf?.forEach((branch) => collect(branch)?.forEach((val) => result.add(val)));

return result.size ? result : null;
};

const set = collect(schema);
return { candidates: set ? [...set] : [], impossible };
}

getQuote(): string {
return this.isSingleQuote ? `'` : `"`;
}
Expand Down
99 changes: 98 additions & 1 deletion test/autoCompletionFix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,7 @@ test1:

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

it('should suggest propertyNames keys from definitions $ref', async () => {
const schema: JSONSchema = {
definitions: {
EventName: {
type: 'string',
title: 'EventName',
enum: ['None', 'Event1', 'Event2'],
},
},
type: 'object',
properties: {
events: {
type: 'object',
propertyNames: { $ref: '#/definitions/EventName' },
},
},
required: ['events'],
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('events:\n ', 1, 2);
expect(completion.items.map((i) => i.label)).to.have.members(['None', 'Event1', 'Event2']);
});

it('should suggest propertyNames candidates from const', async () => {
const schema: JSONSchema = {
type: 'object',
propertyNames: {
const: 'Event0',
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items.map((i) => i.label)).to.have.members(['Event0']);
});

it('should suggest propertyNames candidates from enum', async () => {
const schema: JSONSchema = {
type: 'object',
additionalProperties: true,
propertyNames: {
enum: ['Event1', 'Event2', 'Event3'],
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2', 'Event3']);
});

it('should suggest propertyNames candidates from oneOf', async () => {
const schema: JSONSchema = {
type: 'object',
propertyNames: {
oneOf: [{ const: 'Event1' }, { const: 'Event2' }],
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2']);
});

it('should suggest propertyNames candidates from anyOf', async () => {
const schema: JSONSchema = {
type: 'object',
propertyNames: {
anyOf: [{ const: 'Event1' }, { enum: ['Event2', 'Event3'] }],
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items.map((i) => i.label)).to.have.members(['Event1', 'Event2', 'Event3']);
});

it('should suggest only the intersected propertyNames candidate from allOf (const + enum)', async () => {
const schema: JSONSchema = {
type: 'object',
propertyNames: {
allOf: [{ const: 'One' }, { enum: ['One', 'Two'] }],
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items.map((i) => i.label)).to.have.members(['One']);
expect(completion.items.map((i) => i.label)).to.not.include('Two');
});

it('should not suggest any propertyNames when allOf makes keys impossible (const + const)', async () => {
const schema: JSONSchema = {
type: 'object',
propertyNames: {
allOf: [{ const: 'One' }, { const: 'Two' }],
},
};
schemaProvider.addSchema(SCHEMA_ID, schema);
const completion = await parseSetup('', 0, 0);
expect(completion.items).to.be.empty;
});

describe('String scalar completion comprehensive tests', () => {
const STRING_CASES: {
value: string;
Expand Down