Skip to content

Commit

Permalink
feat: merge snippet props with existing props in yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
p-spacek authored and gorkem committed Apr 9, 2023
1 parent 0080b8c commit 319aaf4
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 18 deletions.
31 changes: 25 additions & 6 deletions src/languageservice/services/yamlCompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const doubleQuotesEscapeRegExp = /[\\]+"/g;

const parentCompletionKind = CompletionItemKind.Class;

const existingProposeItem = '__';

interface ParentCompletionItemOptions {
schema: JSONSchema;
indent?: string;
Expand All @@ -59,6 +61,7 @@ interface CompletionsCollector {
log(message: string): void;
getNumberOfProposals(): number;
result: CompletionList;
proposed: { [key: string]: CompletionItem };
}

interface InsertText {
Expand Down Expand Up @@ -181,7 +184,6 @@ export class YamlCompletion {
}

const proposed: { [key: string]: CompletionItem } = {};
const existingProposeItem = '__';
const collector: CompletionsCollector = {
add: (completionItem: CompletionItem, oneOfSchema: boolean) => {
const addSuggestionForParent = function (completionItem: CompletionItem): void {
Expand Down Expand Up @@ -288,6 +290,7 @@ export class YamlCompletion {
return result.items.length;
},
result,
proposed,
};

if (this.customTags.length > 0) {
Expand Down Expand Up @@ -996,6 +999,7 @@ export class YamlCompletion {
indentFirstObject: false,
shouldIndentWithTab: false,
},
[],
1
);
// add space before default snippet value
Expand Down Expand Up @@ -1474,7 +1478,15 @@ export class YamlCompletion {
});
value = fixedObj;
}
insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings);
const existingProps = Object.keys(collector.proposed).filter(
(proposedProp) => collector.proposed[proposedProp].label === existingProposeItem
);
insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, existingProps);

// if snippet result is empty and value has a real value, don't add it as a completion
if (insertText === '' && value) {
continue;
}
label = label || this.getLabelForSnippetValue(value);
} else if (typeof s.bodyText === 'string') {
let prefix = '',
Expand Down Expand Up @@ -1503,10 +1515,15 @@ export class YamlCompletion {
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, depth?: number): string {
private getInsertTextForSnippetValue(
value: unknown,
separatorAfter: string,
settings: StringifySettings,
existingProps: string[],
depth?: number
): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const replacer = (value: any): string | any => {
const replacer = (value: unknown): string | any => {
if (typeof value === 'string') {
if (value[0] === '^') {
return value.substr(1);
Expand All @@ -1517,7 +1534,9 @@ export class YamlCompletion {
}
return value;
};
return stringifyObject(value, '', replacer, { ...settings, indentation: this.indentation }, depth) + separatorAfter;
return (
stringifyObject(value, '', replacer, { ...settings, indentation: this.indentation, existingProps }, depth) + separatorAfter
);
}

private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void {
Expand Down
29 changes: 19 additions & 10 deletions src/languageservice/utils/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface StringifySettings {

interface StringifySettingsInternal extends StringifySettings {
indentation: string;
existingProps: string[];
}

export function stringifyObject(
Expand All @@ -24,7 +25,7 @@ export function stringifyObject(
if (obj !== null && typeof obj === 'object') {
/**
* When we are autocompleting a snippet from a property we need the indent so everything underneath the property
* is propertly indented. When we are auto completion from a value we don't want the indent because the cursor
* is properly indented. When we are auto completion from a value we don't want the indent because the cursor
* is already in the correct place
*/
const newIndent = (depth === 0 && settings.shouldIndentWithTab) || depth > 0 ? indent + settings.indentation : '';
Expand Down Expand Up @@ -52,24 +53,32 @@ export function stringifyObject(
return '';
}
let result = (depth === 0 && settings.newLineFirst) || depth > 0 ? '\n' : '';
let isFirstProp = true;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];

if (depth === 0 && settings.existingProps.includes(key)) {
// Don't add existing properties to the YAML
continue;
}

const isObject = typeof obj[key] === 'object';
const colonDelimiter = isObject ? ':' : ': '; // add space only when value is primitive
const parentArrayCompensation = isObject && /^\s|-/.test(key) ? settings.indentation : ''; // add extra space if parent is an array
const objectIndent = newIndent + parentArrayCompensation;

// The first child of an array needs to be treated specially, otherwise identations will be off
if (depth === 0 && i === 0 && !settings.indentFirstObject) {
const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, (depth += 1), 0);
result += indent + key + colonDelimiter + value;
const lineBreak = isFirstProp ? '' : '\n'; // break line only if it's not the first property

// The first child of an array needs to be treated specially, otherwise indentations will be off
if (depth === 0 && isFirstProp && !settings.indentFirstObject) {
const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, depth + 1, 0);
result += lineBreak + indent + key + colonDelimiter + value;
} else {
const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, (depth += 1), 0);
result += newIndent + key + colonDelimiter + value;
}
if (i < keys.length - 1) {
result += '\n';
const value = stringifyObject(obj[key], objectIndent, stringifyLiteral, settings, depth + 1, 0);
result += lineBreak + newIndent + key + colonDelimiter + value;
}

isFirstProp = false;
}
return result;
}
Expand Down
38 changes: 36 additions & 2 deletions test/defaultSnippets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,22 @@ describe('Default Snippet Tests', () => {
})
.then(done, done);
});
it('Snippet in array schema should autocomplete correctly inside array item ', (done) => {
it('Snippet in array schema should suggest nothing inside array item if YAML already contains all props', (done) => {
const content = 'array:\n - item1: asd\n item2: asd\n ';
const completion = parseSetup(content, content.length);
completion
.then(function (result) {
assert.equal(result.items.length, 0);
})
.then(done, done);
});
it('Snippet in array schema should suggest only some of the props inside an array item if YAML already contains some of the props', (done) => {
const content = 'array:\n - item1: asd\n ';
const completion = parseSetup(content, content.length);
completion
.then(function (result) {
assert.equal(result.items.length, 1);
assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2');
assert.equal(result.items[0].insertText, 'item2: $2');
assert.equal(result.items[0].label, 'My array item');
})
.then(done, done);
Expand Down Expand Up @@ -167,6 +176,31 @@ describe('Default Snippet Tests', () => {
.then(done, done);
});

it('Snippet in object schema should suggest some of the snippet props because some of them are already in the YAML', (done) => {
const content = 'object:\n key:\n key2: value\n ';
const completion = parseSetup(content, content.length);
completion
.then(function (result) {
assert.notEqual(result.items.length, 0);
assert.equal(result.items[0].insertText, 'key1: ');
assert.equal(result.items[0].label, 'Object item');
assert.equal(result.items[1].insertText, 'key:\n ');
assert.equal(result.items[1].label, 'key');
})
.then(done, done);
});
it('Snippet in object schema should not suggest snippet props because all of them are already in the YAML', (done) => {
const content = 'object:\n key:\n key1: value\n key2: value\n ';
const completion = parseSetup(content, content.length);
completion
.then(function (result) {
assert.equal(result.items.length, 1);
assert.equal(result.items[0].insertText, 'key:\n ');
assert.equal(result.items[0].label, 'key');
})
.then(done, done);
});

it('Snippet in object schema should autocomplete on same line', (done) => {
const content = 'object: '; // len: 9
const completion = parseSetup(content, 8);
Expand Down

0 comments on commit 319aaf4

Please sign in to comment.