Skip to content

Commit

Permalink
Merge edc6efa into 5fae492
Browse files Browse the repository at this point in the history
  • Loading branch information
JPinkney committed Jan 22, 2020
2 parents 5fae492 + edc6efa commit f10865b
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 55 deletions.
81 changes: 58 additions & 23 deletions src/languageservice/services/yamlCompletion.ts
Expand Up @@ -19,7 +19,7 @@ import { LanguageSettings } from '../yamlLanguageService';
import { ResolvedSchema } from 'vscode-json-languageservice/lib/umd/services/jsonSchemaService';
import { JSONCompletion } from 'vscode-json-languageservice/lib/umd/services/jsonCompletion';
import { ClientCapabilities } from 'vscode-languageserver-protocol';
import { stringifyObject } from '../utils/json';
import { stringifyObject, StringifySettings } from '../utils/json';
const localize = nls.loadMessageBundle();

export class YAMLCompletion extends JSONCompletion {
Expand Down Expand Up @@ -234,6 +234,11 @@ export class YAMLCompletion extends JSONCompletion {
if (s.node === node && !s.inverted) {
const schemaProperties = s.schema.properties;
if (schemaProperties) {
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: false,
shouldIndentWithTab: false
}, false);
Object.keys(schemaProperties).forEach((key: string) => {
const propertySchema = schemaProperties[key];
if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) {
Expand Down Expand Up @@ -267,6 +272,25 @@ export class YAMLCompletion extends JSONCompletion {
this.addSchemaValueCompletions(s.schema, separatorAfter, collector, { });
}
}

// Covers the case when we are showing a snippet in an array
if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') {
// For some reason the first item in the array needs to be treated differently, otherwise
// the indentation will not be correct
if (node.properties.length === 1) {
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: false,
shouldIndentWithTab: true
}, false);
} else {
this.collectDefaultSnippets(s.schema, separatorAfter, collector, {
newLineFirst: false,
indentFirstObject: true,
shouldIndentWithTab: false
}, false);
}
}
});
}

Expand Down Expand Up @@ -405,6 +429,17 @@ export class YAMLCompletion extends JSONCompletion {
hasProposals = true;
});
}
this.collectDefaultSnippets(schema, separatorAfter, collector, {
newLineFirst: true,
indentFirstObject: true,
shouldIndentWithTab: true
}, schema.type === 'array');
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
}
}

private collectDefaultSnippets(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, isArray: boolean) {
if (Array.isArray(schema.defaultSnippets)) {
schema.defaultSnippets.forEach(s => {
let type = schema.type;
Expand All @@ -413,24 +448,13 @@ export class YAMLCompletion extends JSONCompletion {
let insertText: string;
let filterText: string;
if (isDefined(value)) {
let type = schema.type;
for (let i = arrayDepth; i > 0; i--) {
value = [value];
type = 'array';
}
insertText = this.getInsertTextForSnippetValue(value, separatorAfter);
insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, isArray);
label = label || this.getLabelForSnippetValue(value);
} else if (typeof s.bodyText === 'string') {
let prefix = '', suffix = '', indent = '';
for (let i = arrayDepth; i > 0; i--) {
prefix = prefix + indent + '[\n';
suffix = suffix + '\n' + indent + ']';
indent += '\t';
type = 'array';
}
insertText = prefix + indent + s.bodyText.split('\n').join('\n' + indent) + suffix + separatorAfter;
label = label || insertText,
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
label = label || insertText;
filterText = insertText.replace(/[\n]/g, ''); // remove new lines
}
collector.add({
kind: this.getSuggestionKind(type),
Expand All @@ -440,26 +464,33 @@ export class YAMLCompletion extends JSONCompletion {
insertTextFormat: InsertTextFormat.Snippet,
filterText
});
hasProposals = true;
});
}
if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1);
}
}

// tslint:disable-next-line:no-any
private getInsertTextForSnippetValue(value: any, separatorAfter: string): string {
private getInsertTextForSnippetValue(value: any, separatorAfter: string, settings: StringifySettings, isArray?: boolean): string {
// tslint:disable-next-line:no-any
const replacer = (value: any) => {
if (typeof value === 'string') {
if (value[0] === '^') {
return value.substr(1);
}
}
return JSON.stringify(value);
return value;
};
return stringifyObject(value, '', replacer) + separatorAfter;
if (isArray && typeof value === 'object' && value !== null) {
const fixedObj = { };
Object.keys(value).forEach((val, index) => {
if (index === 0 && !val.startsWith('-')) {
fixedObj[`- ${val}`] = value[val];
} else {
fixedObj[` ${val}`] = value[val];
}
});
value = fixedObj;
}
return stringifyObject(value, '', replacer, settings) + separatorAfter;
}

// tslint:disable-next-line:no-any
Expand Down Expand Up @@ -632,7 +663,11 @@ export class YAMLCompletion extends JSONCompletion {
if (propertySchema.defaultSnippets.length === 1) {
const body = propertySchema.defaultSnippets[0].body;
if (isDefined(body)) {
value = this.getInsertTextForSnippetValue(body, '');
value = this.getInsertTextForSnippetValue(body, '', {
newLineFirst: true,
indentFirstObject: false,
shouldIndentWithTab: false
});
}
}
nValueProposals += propertySchema.defaultSnippets.length;
Expand Down
57 changes: 25 additions & 32 deletions src/languageservice/utils/json.ts
Expand Up @@ -3,42 +3,35 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

/**
* THIS IS GOING TO BE THE ONES THATS USED WHEN YOU AUTOCOMPLETE FROM A SCALAR
*/
// tslint:disable-next-line: no-any
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string): string {
if (obj !== null && typeof obj === 'object') {
const newIndent = indent + '\t';
if (Array.isArray(obj)) {
if (obj.length === 0) {
return '[]';
}
let result = '[\n';
for (let i = 0; i < obj.length; i++) {
result += newIndent + stringifyObject(obj[i], newIndent, stringifyLiteral);
if (i < obj.length - 1) {
result += ',';
}
result += '\n';
}
result += indent + ']';
return result;
} else {
const keys = Object.keys(obj);
if (keys.length === 0) {
return '{}';
}
let result = '{\n';
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
export interface StringifySettings {
newLineFirst: boolean;
indentFirstObject: boolean;
shouldIndentWithTab: boolean;
}

result += newIndent + JSON.stringify(key) + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral);
if (i < keys.length - 1) {
result += ',';
}
result += '\n';
export function stringifyObject(obj: any, indent: string, stringifyLiteral: (val: any) => string, settings: StringifySettings): string {
if (obj !== null && typeof obj === 'object') {
const newIndent = settings.shouldIndentWithTab ? (indent + '\t') : indent;
const keys = Object.keys(obj);
if (keys.length === 0) {
return '';
}
let result = settings.newLineFirst ? '\n' : '';
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (i === 0 && !settings.indentFirstObject) {
result += indent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
} else {
result += newIndent + key + ': ' + stringifyObject(obj[key], newIndent, stringifyLiteral, settings);
}
result += indent + '}';
return result;
result += '\n';
}
result += indent;
return result;
}
return stringifyLiteral(obj);
}
126 changes: 126 additions & 0 deletions test/defaultSnippets.test.ts
@@ -0,0 +1,126 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Red Hat. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument } from 'vscode-languageserver';
import { getLanguageService } from '../src/languageservice/yamlLanguageService';
import { toFsPath, schemaRequestService, workspaceContext } from './utils/testHelper';
import assert = require('assert');
import path = require('path');

const languageService = getLanguageService(schemaRequestService, workspaceContext, [], null);

const languageSettings = {
schemas: [],
completion: true
};

const uri = toFsPath(path.join(__dirname, './fixtures/defaultSnippets.json'));
const fileMatch = ['*.yml', '*.yaml'];
languageSettings.schemas.push({ uri, fileMatch: fileMatch });
languageService.configure(languageSettings);

suite('Default Snippet Tests', () => {

describe('Snippet Tests', function () {

function setup(content: string) {
return TextDocument.create('file://~/Desktop/vscode-k8s/test.yaml', 'yaml', 0, content);
}

function parseSetup(content: string, position: number) {
const testTextDocument = setup(content);
return languageService.doComplete(testTextDocument, testTextDocument.positionAt(position), false);
}

it('Snippet in array schema should autocomplete with -', done => {
const content = 'array:\n - ';
const completion = parseSetup(content, 11);
completion.then(function (result) {
assert.equal(result.items.length, 1);
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
assert.equal(result.items[0].label, 'My array item');
}).then(done, done);
});

it('Snippet in array schema should autocomplete on next line with depth', done => {
const content = 'array:\n - item1:\n - ';
const completion = parseSetup(content, 24);
completion.then(function (result) {
assert.equal(result.items.length, 1);
assert.equal(result.items[0].insertText, 'item1: $1\n\titem2: $2\n');
assert.equal(result.items[0].label, 'My array item');
}).then(done, done);
});

it('Snippet in array schema should autocomplete correctly after ', done => {
const content = 'array:\n - item1: asd\n - item2: asd\n ';
const completion = parseSetup(content, 40);
completion.then(function (result) {
assert.equal(result.items.length, 1);
assert.equal(result.items[0].insertText, 'item1: $1\nitem2: $2\n');
assert.equal(result.items[0].label, 'My array item');
}).then(done, done);
});

it('Snippet in array schema should autocomplete on same line as array', done => {
const content = 'array: ';
const completion = parseSetup(content, 7);
completion.then(function (result) {
assert.equal(result.items.length, 1);
}).then(done, done);
});

it('Snippet in object schema should autocomplete on next line ', done => {
const content = 'object:\n ';
const completion = parseSetup(content, 11);
completion.then(function (result) {
assert.equal(result.items.length, 2);
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
assert.equal(result.items[0].label, 'Object item');
assert.equal(result.items[1].insertText, 'key:\n\t$1');
assert.equal(result.items[1].label, 'key');
}).then(done, done);
});

it('Snippet in object schema should autocomplete on next line with depth', done => {
const content = 'object:\n key:\n ';
const completion = parseSetup(content, 20);
completion.then(function (result) {
assert.notEqual(result.items.length, 0);
assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2\n');
assert.equal(result.items[0].label, 'Object item');
assert.equal(result.items[1].insertText, 'key:\n\t$1');
assert.equal(result.items[1].label, 'key');
}).then(done, done);
});

it('Snippet in object schema should autocomplete on same line', done => {
const content = 'object: ';
const completion = parseSetup(content, 8);
completion.then(function (result) {
assert.equal(result.items.length, 1);
}).then(done, done);
});

it('Snippet in string schema should autocomplete on same line', done => {
const content = 'string: ';
const completion = parseSetup(content, 8);
completion.then(function (result) {
assert.notEqual(result.items.length, 0);
assert.equal(result.items[0].insertText, 'test $1');
assert.equal(result.items[0].label, 'My string item');
}).then(done, done);
});

it('Snippet in boolean schema should autocomplete on same line', done => {
const content = 'boolean: ';
const completion = parseSetup(content, 9);
completion.then(function (result) {
assert.notEqual(result.items.length, 0);
assert.equal(result.items[0].label, 'My boolean item');
assert.equal(result.items[0].insertText, 'false');
}).then(done, done);
});
});
});
55 changes: 55 additions & 0 deletions test/fixtures/defaultSnippets.json
@@ -0,0 +1,55 @@
{
"type": "object",
"properties": {
"object": {
"type": "object",
"defaultSnippets": [
{
"label": "Object item",
"description": "Binds a key to a command for a given state",
"body": { "key1": "$1", "key2": "$2" }
}
],
"properties": {
"key": {
"$ref": "#/properties/object"
}
}
},
"array": {
"type": "array",
"defaultSnippets": [
{
"label": "My array item",
"body": { "item1": "$1", "item2": "$2" }
}
],
"items": {
"item1": {
"$ref": "#/properties/array"
},
"item2": {
"$ref": "#/properties/array"
}
}
},
"string": {
"type": "string",
"defaultSnippets": [
{
"label": "My string item",
"bodyText": "test $1"
}
]
},
"boolean": {
"type": "boolean",
"defaultSnippets": [
{
"label": "My boolean item",
"bodyText": "false"
}
]
}
}
}

0 comments on commit f10865b

Please sign in to comment.