Skip to content

Commit 68ccc63

Browse files
authored
Merge pull request #221 from redhat-developer/defaultSnippets-tests
Made default snippets yaml-like and added tests
2 parents e7ac5f8 + 1bbef42 commit 68ccc63

4 files changed

Lines changed: 263 additions & 55 deletions

File tree

src/languageservice/services/yamlCompletion.ts

Lines changed: 60 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { LanguageSettings } from '../yamlLanguageService';
1919
import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
2020
import { JSONCompletion } from 'vscode-json-languageservice/lib/umd/services/jsonCompletion';
2121
import { ClientCapabilities } from 'vscode-languageserver-protocol';
22-
import { stringifyObject } from '../utils/json';
22+
import { stringifyObject, StringifySettings } from '../utils/json';
2323
const localize = nls.loadMessageBundle();
2424

2525
export class YAMLCompletion extends JSONCompletion {
@@ -234,6 +234,11 @@ export class YAMLCompletion extends JSONCompletion {
234234
if (s.node === node && !s.inverted) {
235235
const schemaProperties = s.schema.properties;
236236
if (schemaProperties) {
237+
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
238+
newLineFirst: false,
239+
indentFirstObject: false,
240+
shouldIndentWithTab: false
241+
}, false);
237242
Object.keys(schemaProperties).forEach((key: string) => {
238243
const propertySchema = schemaProperties[key];
239244
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) {
@@ -267,6 +272,25 @@ export class YAMLCompletion extends JSONCompletion {
267272
this.addSchemaValueCompletions(s.schema, separatorAfter, collector, { });
268273
}
269274
}
275+
276+
// Covers the case when we are showing a snippet in an array
277+
if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') {
278+
// For some reason the first item in the array needs to be treated differently, otherwise
279+
// the indentation will not be correct
280+
if (node.properties.length === 1) {
281+
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
282+
newLineFirst: false,
283+
indentFirstObject: false,
284+
shouldIndentWithTab: true
285+
}, false);
286+
} else {
287+
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
288+
newLineFirst: false,
289+
indentFirstObject: true,
290+
shouldIndentWithTab: false
291+
}, false);
292+
}
293+
}
270294
});
271295
}
272296

@@ -405,6 +429,17 @@ export class YAMLCompletion extends JSONCompletion {
405429
hasProposals = true;
406430
});
407431
}
432+
this.collectDefaultSnippets(schema, separatorAfter, collector, {
433+
newLineFirst: true,
434+
indentFirstObject: true,
435+
shouldIndentWithTab: true
436+
}, schema.type === 'array');
437+
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
438+
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
439+
}
440+
}
441+
442+
private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean) {
408443
if (Array.isArray(schema.defaultSnippets)) {
409444
schema.defaultSnippets.forEach(s => {
410445
let type = schema.type;
@@ -413,24 +448,13 @@ export class YAMLCompletion extends JSONCompletion {
413448
let insertText: string;
414449
let filterText: string;
415450
if (isDefined(value)) {
416-
let type = schema.type;
417-
for (let i = arrayDepth; i > 0; i--) {
418-
value = [value];
419-
type = 'array';
420-
}
421-
insertText = this.getInsertTextForSnippetValue(value, separatorAfter);
451+
insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, isArray);
422452
label = label || this.getLabelForSnippetValue(value);
423453
} else if (typeof s.bodyText === 'string') {
424454
let prefix = '', suffix = '', indent = '';
425-
for (let i = arrayDepth; i > 0; i--) {
426-
prefix = prefix + indent + '[\n';
427-
suffix = suffix + '\n' + indent + ']';
428-
indent += '\t';
429-
type = 'array';
430-
}
431455
insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter;
432-
label = label || insertText,
433-
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
456+
label = label || insertText;
457+
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
434458
}
435459
collector.add({
436460
kind: this.getSuggestionKind(type),
@@ -440,26 +464,35 @@ export class YAMLCompletion extends JSONCompletion {
440464
insertTextFormat: InsertTextFormat.Snippet,
441465
filterText
442466
});
443-
hasProposals = true;
444467
});
445468
}
446-
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
447-
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
448-
}
449469
}
450470

451471
// tslint:disable-next-line:no-any
452-
private getInsertTextForSnippetValue(value: any, separatorAfter: string): string {
472+
private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean): string {
453473
// tslint:disable-next-line:no-any
454474
const replacer = (value: any) => {
455475
if (typeof value === 'string') {
456476
if (value[0] === '^') {
457477
return value.substr(1);
458478
}
459479
}
460-
return JSON.stringify(value);
480+
return value;
461481
};
462-
return stringifyObject(value, '', replacer) + separatorAfter;
482+
// If it is an array then we need to manually indent the keys
483+
// of that array item
484+
if (isArray && typeof value === 'object' && value !== null) {
485+
const fixedObj = { };
486+
Object.keys(value).forEach((val, index) => {
487+
if (index === 0 && !val.startsWith('-')) {
488+
fixedObj[`- ${val}`] = value[val];
489+
} else {
490+
fixedObj[` ${val}`] = value[val];
491+
}
492+
});
493+
value = fixedObj;
494+
}
495+
return stringifyObject(value, '', replacer, settings) + separatorAfter;
463496
}
464497

465498
// tslint:disable-next-line:no-any
@@ -632,7 +665,11 @@ export class YAMLCompletion extends JSONCompletion {
632665
if (propertySchema.defaultSnippets.length === 1) {
633666
const body = propertySchema.defaultSnippets[0].body;
634667
if (isDefined(body)) {
635-
value = this.getInsertTextForSnippetValue(body, '');
668+
value = this.getInsertTextForSnippetValue(body, '', {
669+
newLineFirst: true,
670+
indentFirstObject: false,
671+
shouldIndentWithTab: false
672+
});
636673
}
637674
}
638675
nValueProposals += propertySchema.defaultSnippets.length;

src/languageservice/utils/json.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,31 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
// tslint:disable-next-line: no-any
7-
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string): string {
8-
if (obj !== null && typeof obj === 'object') {
9-
const newIndent = indent + '\t';
10-
if (Array.isArray(obj)) {
11-
if (obj.length === 0) {
12-
return '[]';
13-
}
14-
let result = '[\n';
15-
for (let i = 0; i < obj.length; i++) {
16-
result += newIndent + stringifyObject(obj[i], newIndent, stringifyLiteral);
17-
if (i < obj.length - 1) {
18-
result += ',';
19-
}
20-
result += '\n';
21-
}
22-
result += indent + ']';
23-
return result;
24-
} else {
25-
const keys = Object.keys(obj);
26-
if (keys.length === 0) {
27-
return '{}';
28-
}
29-
let result = '{\n';
30-
for (let i = 0; i < keys.length; i++) {
31-
const key = keys[i];
7+
export interface StringifySettings {
8+
newLineFirst: boolean;
9+
indentFirstObject: boolean;
10+
shouldIndentWithTab: boolean;
11+
}
3212

33-
result += newIndent + JSON.stringify(key) + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral);
34-
if (i < keys.length - 1) {
35-
result += ',';
36-
}
37-
result += '\n';
13+
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings): string {
14+
if (obj !== null && typeof obj === 'object') {
15+
const newIndent = settings.shouldIndentWithTab ? (indent + '\t') : indent;
16+
const keys = Object.keys(obj);
17+
if (keys.length === 0) {
18+
return '';
19+
}
20+
let result = settings.newLineFirst ? '\n' : '';
21+
for (let i = 0; i < keys.length; i++) {
22+
const key = keys[i];
23+
if (i === 0 && !settings.indentFirstObject) {
24+
result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
25+
} else {
26+
result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
3827
}
39-
result += indent + '}';
40-
return result;
28+
result += '\n';
4129
}
30+
result += indent;
31+
return result;
4232
}
4333
return stringifyLiteral(obj);
4434
}

test/defaultSnippets.test.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Red Hat. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
import { TextDocument } from 'vscode-languageserver';
6+
import { getLanguageService } from '../src/languageservice/yamlLanguageService';
7+
import { toFsPath, schemaRequestService, workspaceContext } from './utils/testHelper';
8+
import assert = require('assert');
9+
import path = require('path');
10+
11+
const languageService = getLanguageService(schemaRequestService, workspaceContext, [], null);
12+
13+
const languageSettings = {
14+
schemas: [],
15+
completion: true
16+
};
17+
18+
const uri = toFsPath(path.join(__dirname, './fixtures/defaultSnippets.json'));
19+
const fileMatch = ['*.yml', '*.yaml'];
20+
languageSettings.schemas.push({ uri, fileMatch: fileMatch });
21+
languageService.configure(languageSettings);
22+
23+
suite('Default Snippet Tests', () => {
24+
25+
describe('Snippet Tests', function () {
26+
27+
function setup(content: string) {
28+
return TextDocument.create('file://~/Desktop/vscode-k8s/test.yaml', 'yaml', 0, content);
29+
}
30+
31+
function parseSetup(content: string, position: number) {
32+
const testTextDocument = setup(content);
33+
return languageService.doComplete(testTextDocument, testTextDocument.positionAt(position), false);
34+
}
35+
36+
it('Snippet in array schema should autocomplete with -', done => {
37+
const content = 'array:\n - ';
38+
const completion = parseSetup(content, 11);
39+
completion.then(function (result) {
40+
assert.equal(result.items.length, 1);
41+
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
42+
assert.equal(result.items[0].label, 'My array item');
43+
}).then(done, done);
44+
});
45+
46+
it('Snippet in array schema should autocomplete on next line with depth', done => {
47+
const content = 'array:\n - item1:\n - ';
48+
const completion = parseSetup(content, 24);
49+
completion.then(function (result) {
50+
assert.equal(result.items.length, 1);
51+
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
52+
assert.equal(result.items[0].label, 'My array item');
53+
}).then(done, done);
54+
});
55+
56+
it('Snippet in array schema should autocomplete correctly after ', done => {
57+
const content = 'array:\n - item1: asd\n - item2: asd\n ';
58+
const completion = parseSetup(content, 40);
59+
completion.then(function (result) {
60+
assert.equal(result.items.length, 1);
61+
assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2\n');
62+
assert.equal(result.items[0].label, 'My array item');
63+
}).then(done, done);
64+
});
65+
66+
it('Snippet in array schema should autocomplete on same line as array', done => {
67+
const content = 'array: ';
68+
const completion = parseSetup(content, 7);
69+
completion.then(function (result) {
70+
assert.equal(result.items.length, 1);
71+
}).then(done, done);
72+
});
73+
74+
it('Snippet in object schema should autocomplete on next line ', done => {
75+
const content = 'object:\n ';
76+
const completion = parseSetup(content, 11);
77+
completion.then(function (result) {
78+
assert.equal(result.items.length, 2);
79+
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
80+
assert.equal(result.items[0].label, 'Object item');
81+
assert.equal(result.items[1].insertText, 'key:\n\t$1');
82+
assert.equal(result.items[1].label, 'key');
83+
}).then(done, done);
84+
});
85+
86+
it('Snippet in object schema should autocomplete on next line with depth', done => {
87+
const content = 'object:\n key:\n ';
88+
const completion = parseSetup(content, 20);
89+
completion.then(function (result) {
90+
assert.notEqual(result.items.length, 0);
91+
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
92+
assert.equal(result.items[0].label, 'Object item');
93+
assert.equal(result.items[1].insertText, 'key:\n\t$1');
94+
assert.equal(result.items[1].label, 'key');
95+
}).then(done, done);
96+
});
97+
98+
it('Snippet in object schema should autocomplete on same line', done => {
99+
const content = 'object: ';
100+
const completion = parseSetup(content, 8);
101+
completion.then(function (result) {
102+
assert.equal(result.items.length, 1);
103+
}).then(done, done);
104+
});
105+
106+
it('Snippet in string schema should autocomplete on same line', done => {
107+
const content = 'string: ';
108+
const completion = parseSetup(content, 8);
109+
completion.then(function (result) {
110+
assert.notEqual(result.items.length, 0);
111+
assert.equal(result.items[0].insertText, 'test $1');
112+
assert.equal(result.items[0].label, 'My string item');
113+
}).then(done, done);
114+
});
115+
116+
it('Snippet in boolean schema should autocomplete on same line', done => {
117+
const content = 'boolean: ';
118+
const completion = parseSetup(content, 9);
119+
completion.then(function (result) {
120+
assert.notEqual(result.items.length, 0);
121+
assert.equal(result.items[0].label, 'My boolean item');
122+
assert.equal(result.items[0].insertText, 'false');
123+
}).then(done, done);
124+
});
125+
});
126+
});

test/fixtures/defaultSnippets.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"object": {
5+
"type": "object",
6+
"defaultSnippets": [
7+
{
8+
"label": "Object item",
9+
"description": "Binds a key to a command for a given state",
10+
"body": { "key1": "$1", "key2": "$2" }
11+
}
12+
],
13+
"properties": {
14+
"key": {
15+
"$ref": "#/properties/object"
16+
}
17+
}
18+
},
19+
"array": {
20+
"type": "array",
21+
"defaultSnippets": [
22+
{
23+
"label": "My array item",
24+
"body": { "item1": "$1", "item2": "$2" }
25+
}
26+
],
27+
"items": {
28+
"item1": {
29+
"$ref": "#/properties/array"
30+
},
31+
"item2": {
32+
"$ref": "#/properties/array"
33+
}
34+
}
35+
},
36+
"string": {
37+
"type": "string",
38+
"defaultSnippets": [
39+
{
40+
"label": "My string item",
41+
"bodyText": "test $1"
42+
}
43+
]
44+
},
45+
"boolean": {
46+
"type": "boolean",
47+
"defaultSnippets": [
48+
{
49+
"label": "My boolean item",
50+
"bodyText": "false"
51+
}
52+
]
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)