Skip to content
Open
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ Schema validation supports JSON Schema `draft-04`, `draft-07`, `2019-09`, and `2

Completion and hover content are schema-driven. See [Associating schemas](#associating-schemas) for configuration details.

Snippet completions require the client capability `textDocument.completion.completionItem.snippetSupport`.
When it is absent or `false`, generated completions insert plain text using schema defaults,
without tab stops. Schema-provided `defaultSnippets` are offered only to clients that support snippets.
Clients that support snippets retain editable placeholders.

## Language server settings

The server supports the following settings supplied by LSP clients:
Expand Down Expand Up @@ -274,10 +279,7 @@ For multiple file patterns:

```json
{
"yaml.disableSchemaDetection": [
"some.yaml",
"**/.github/workflows/*.yaml"
]
"yaml.disableSchemaDetection": ["some.yaml", "**/.github/workflows/*.yaml"]
}
```

Expand Down
3 changes: 3 additions & 0 deletions src/languageservice/jsonLanguageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ export interface ClientCapabilities {
* capabilities.
*/
completionItem?: {
/** Whether the client supports snippet completion text. */
snippetSupport?: boolean;
/**
* Client supports the follow content formats for the documentation
* property. The order describes the preferred format of the client.
Expand Down Expand Up @@ -362,6 +364,7 @@ export const ClientCapabilities: { LATEST: ClientCapabilities } = {
textDocument: {
completion: {
completionItem: {
snippetSupport: true,
documentationFormat: [MarkupKind.Markdown, MarkupKind.PlainText],
commitCharactersSupport: true,
labelDetailsSupport: true,
Expand Down
102 changes: 62 additions & 40 deletions src/languageservice/services/yamlCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ export class YamlCompletion {
this.parentSkeletonSelectedFirst = languageSettings.parentSkeletonSelectedFirst;
}

private get supportsSnippets(): boolean {
return !!this.clientCapabilities.textDocument?.completion?.completionItem?.snippetSupport;
}

private get insertTextFormat(): InsertTextFormat {
return this.supportsSnippets ? InsertTextFormat.Snippet : InsertTextFormat.PlainText;
}

private tabStop(index: number, value?: string): string {
if (!this.supportsSnippets) {
return value ?? '';
}
return value === undefined ? '$' + index : '${' + index + ':' + value + '}';
}

async doComplete(document: TextDocument, position: Position, isKubernetes = false, doComplete = true): Promise<CompletionList> {
const result = CompletionList.create([], false);
if (!this.completionEnabled) {
Expand Down Expand Up @@ -258,7 +273,7 @@ export class YamlCompletion {
completionItem.insertText = `${key}: ${this.getQuote()}\\${char}${this.getQuote()}`;
}
// trim $1 from end of completion
if (completionItem.insertText.endsWith('$1') && !isForParentCompletion) {
if (this.supportsSnippets && completionItem.insertText.endsWith('$1') && !isForParentCompletion) {
completionItem.insertText = completionItem.insertText.substr(0, completionItem.insertText.length - 2);
}
if (overwriteRange && overwriteRange.start.line === overwriteRange.end.line) {
Expand Down Expand Up @@ -554,7 +569,7 @@ export class YamlCompletion {
kind: CompletionItemKind.Property,
label: currentWord,
insertText: this.getInsertTextForProperty(currentWord, null, ''),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
});
}
}
Expand Down Expand Up @@ -612,6 +627,8 @@ export class YamlCompletion {
const newValues = Array.prototype.concat(existingValues, addingValues);
if (!newValues.length) {
return undefined;
} else if (!this.supportsSnippets) {
return `${label}: ${newValues[0]}`;
} else if (newValues.length === 1) {
return `${label}: \${1:${newValues[0]}}`;
} else {
Expand Down Expand Up @@ -655,13 +672,15 @@ export class YamlCompletion {
if (this.isParentCompletionItem(completionItem)) {
const indent = completionItem.parent.indent || '';

const reindexedTexts = reindexText(completionItem.parent.insertTexts);
const reindexedTexts = this.supportsSnippets
? reindexText(completionItem.parent.insertTexts)
: completionItem.parent.insertTexts;

// add indent to each object property and join completion item texts
let insertText = reindexedTexts.join(`\n${indent}`);

// trim $1 from end of completion
if (insertText.endsWith('$1')) {
if (this.supportsSnippets && insertText.endsWith('$1')) {
insertText = insertText.substring(0, insertText.length - 2);
}

Expand Down Expand Up @@ -807,7 +826,7 @@ export class YamlCompletion {
kind: CompletionItemKind.Property,
label: key,
insertText,
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '',
},
didOneOfSchemaMatches
Expand All @@ -824,7 +843,7 @@ export class YamlCompletion {
identCompensation + this.indentation,
true
),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '',
parent: {
schema: schema.schema,
Expand Down Expand Up @@ -871,8 +890,8 @@ export class YamlCompletion {
collector.add({
kind: CompletionItemKind.Property,
label,
insertText: '$' + `{1:${label}}: `,
insertTextFormat: InsertTextFormat.Snippet,
insertText: this.tabStop(1, label) + ': ',
insertTextFormat: this.insertTextFormat,
documentation: doc,
});
}
Expand Down Expand Up @@ -1015,7 +1034,7 @@ export class YamlCompletion {
label: l10n.t('- (array item) ') + (schemaType || index),
documentation: documentation,
insertText: insertText,
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
});
}

Expand Down Expand Up @@ -1043,7 +1062,7 @@ export class YamlCompletion {
type = 'anyOf';
}
}
if (Array.isArray(propertySchema.defaultSnippets)) {
if (this.supportsSnippets && Array.isArray(propertySchema.defaultSnippets)) {
if (propertySchema.defaultSnippets.length === 1) {
const body = propertySchema.defaultSnippets[0].body;
if (isDefined(body)) {
Expand Down Expand Up @@ -1118,7 +1137,7 @@ export class YamlCompletion {
case 'boolean':
case 'string':
case 'anyOf':
value = ' $1';
value = ' ' + this.tabStop(1);
break;
case 'object':
value = `\n${indent}`;
Expand All @@ -1128,18 +1147,18 @@ export class YamlCompletion {
break;
case 'number':
case 'integer':
value = ' ${1:0}';
value = ' ' + this.tabStop(1, '0');
break;
case 'null':
value = ' ${1:null}';
value = ' ' + this.tabStop(1, 'null');
break;
default:
return propertyText;
}
}
}
if (!value || (nValueProposals > 1 && !hasRequiredDefault)) {
value = ' $1';
value = ' ' + this.tabStop(1);
}
return resultText + value + separatorAfter;
}
Expand All @@ -1152,7 +1171,7 @@ export class YamlCompletion {
): InsertText {
let insertText = '';
if (!schema.properties) {
insertText = `${indent}$${insertIndex++}\n`;
insertText = `${indent}${this.tabStop(insertIndex++)}\n`;
return { insertText, insertIndex };
}

Expand Down Expand Up @@ -1182,9 +1201,9 @@ export class YamlCompletion {
if (type === 'string') {
value = toYamlStringScalar(value);
}
insertText += `${indent}${key}: \${${insertIndex++}:${value}}\n`;
insertText += `${indent}${key}: ${this.tabStop(insertIndex++, String(value))}\n`;
} else {
insertText += `${indent}${key}: $${insertIndex++}\n`;
insertText += `${indent}${key}: ${this.tabStop(insertIndex++)}\n`;
}
break;
}
Expand Down Expand Up @@ -1225,10 +1244,10 @@ export class YamlCompletion {
insertText += `${indent}${
//added quote if key is null
key === 'null' ? this.getInsertTextForValue(key, '', 'string') : key
}: \${${insertIndex++}:${propertySchema.default}}\n`;
}: ${this.tabStop(insertIndex++, String(propertySchema.default))}\n`;
break;
case 'string':
insertText += `${indent}${key}: \${${insertIndex++}:${toYamlStringScalar(propertySchema.default)}}\n`;
insertText += `${indent}${key}: ${this.tabStop(insertIndex++, toYamlStringScalar(propertySchema.default))}\n`;
break;
case 'array':
case 'object':
Expand All @@ -1238,7 +1257,7 @@ export class YamlCompletion {
}
});
if (insertText.trim().length === 0) {
insertText = `${indent}$${insertIndex++}\n`;
insertText = `${indent}${this.tabStop(insertIndex++)}\n`;
}
insertText = insertText.trimEnd() + separatorAfter;
return { insertText, insertIndex };
Expand All @@ -1248,7 +1267,7 @@ export class YamlCompletion {
private getInsertTextForArray(schema: any, separatorAfter: string, insertIndex = 1, indent = this.indentation): InsertText {
let insertText = '';
if (!schema) {
insertText = `$${insertIndex++}`;
insertText = this.tabStop(insertIndex++);
return { insertText, insertIndex };
}
let type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
Expand All @@ -1265,14 +1284,14 @@ export class YamlCompletion {
} else {
switch (schema.type) {
case 'boolean':
insertText = `\${${insertIndex++}:false}`;
insertText = this.tabStop(insertIndex++, 'false');
break;
case 'number':
case 'integer':
insertText = `\${${insertIndex++}:0}`;
insertText = this.tabStop(insertIndex++, '0');
break;
case 'string':
insertText = `\${${insertIndex++}}`;
insertText = this.supportsSnippets ? `\${${insertIndex++}}` : this.tabStop(insertIndex++);
break;
case 'object':
{
Expand All @@ -1291,24 +1310,27 @@ export class YamlCompletion {
switch (typeof value) {
case 'object':
if (value === null) {
return '${1:null}' + separatorAfter;
return this.tabStop(1, 'null') + separatorAfter;
}
return this.getInsertTextForValue(value, separatorAfter, type);
case 'string': {
if (type === 'number' || type === 'integer') {
return '${1:' + value + '}' + separatorAfter;
return this.tabStop(1, String(value)) + separatorAfter;
}
const snippetValue = this.getInsertTextForPlainText(toYamlStringScalar(value));
return '${1:' + snippetValue + '}' + separatorAfter;
return this.tabStop(1, snippetValue) + separatorAfter;
}
case 'number':
case 'boolean':
return '${1:' + value + '}' + separatorAfter;
return this.tabStop(1, String(value)) + separatorAfter;
}
return this.getInsertTextForValue(value, separatorAfter, type);
}

private getInsertTextForPlainText(text: string): string {
if (!this.supportsSnippets) {
return text;
}
return text.replace(/\\(?=[$}\\])/g, '\\\\').replace(/[$}]/g, '\\$&');
}

Expand Down Expand Up @@ -1342,20 +1364,20 @@ export class YamlCompletion {
if (Array.isArray(value)) {
let insertText = '\n';
for (const arrValue of value) {
insertText += `${indent}- \${${navOrder.index++}:${arrValue}}\n`;
insertText += `${indent}- ${this.tabStop(navOrder.index++, String(arrValue))}\n`;
}
return insertText;
} else if (typeof value === 'object') {
let insertText = '\n';
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
const element = value[key];
insertText += `${indent}\${${navOrder.index++}:${key}}:`;
insertText += `${indent}${this.tabStop(navOrder.index++, key)}:`;
let valueTemplate;
if (typeof element === 'object') {
valueTemplate = `${this.getInsertTemplateForValue(element, indent + this.indentation, navOrder, separatorAfter)}`;
} else {
valueTemplate = ` \${${navOrder.index++}:${this.getInsertTextForPlainText(element + separatorAfter)}}\n`;
valueTemplate = ` ${this.tabStop(navOrder.index++, this.getInsertTextForPlainText(element + separatorAfter))}\n`;
}
insertText += `${valueTemplate}`;
}
Expand Down Expand Up @@ -1438,7 +1460,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind(type),
label,
insertText: this.getInsertTextForValue(value, separatorAfter, type),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
detail: l10n.t('Default value'),
});
hasProposals = true;
Expand All @@ -1455,7 +1477,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind(type),
label: this.getLabelForValue(value),
insertText: this.getInsertTextForValue(value, separatorAfter, type),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
});
hasProposals = true;
});
Expand All @@ -1482,7 +1504,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind(schema.type),
label: this.getLabelForValue(schema.const),
insertText: this.getInsertTextForValue(schema.const, separatorAfter, schema.type),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: this.fromMarkup(schema.markdownDescription) || schema.description,
});
}
Expand All @@ -1503,7 +1525,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind(schema.type),
label: this.getLabelForValue(enm),
insertText: this.getInsertTextForValue(enm, separatorAfter, schema.type),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: documentation,
});
}
Expand All @@ -1530,7 +1552,7 @@ export class YamlCompletion {
settings: StringifySettings,
arrayDepth = 0
): void {
if (Array.isArray(schema.defaultSnippets)) {
if (this.supportsSnippets && Array.isArray(schema.defaultSnippets)) {
for (const s of schema.defaultSnippets) {
let type = schema.type;
let value = s.body;
Expand Down Expand Up @@ -1581,7 +1603,7 @@ export class YamlCompletion {
sortText: s.sortText || s.label,
documentation: this.fromMarkup(s.markdownDescription) || s.description,
insertText,
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
filterText,
});
}
Expand Down Expand Up @@ -1617,7 +1639,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind('boolean'),
label: value ? 'true' : 'false',
insertText: this.getInsertTextForValue(value, separatorAfter, 'boolean'),
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: '',
});
}
Expand All @@ -1627,7 +1649,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind('null'),
label: 'null',
insertText: 'null' + separatorAfter,
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: '',
});
}
Expand All @@ -1652,7 +1674,7 @@ export class YamlCompletion {
kind: this.getSuggestionKind('string'),
label: label,
insertText: label + separatorAfter,
insertTextFormat: InsertTextFormat.Snippet,
insertTextFormat: this.insertTextFormat,
documentation: '',
});
}
Expand Down
Loading
Loading