diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 8a3e5e74..8fccc8a4 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -189,9 +189,8 @@ export class YAMLCompletion { } // proposals for values - let types: { [type: string]: boolean } = {}; if (newSchema) { - this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector, types); + this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector); } if (this.contributions.length > 0) { this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); @@ -225,11 +224,19 @@ export class YAMLCompletion { } }); } - } + // Error fix + // If this is a array of string/boolean/number + // test: + // - item1 + // it will treated as a property key since `:` has been appended + if (node.type === 'object' && node.parent && node.parent.type === 'array' && s.schema.type !== 'object') { + this.addSchemaValueCompletions(s.schema, collector, separatorAfter) + } + } }); } - private getValueCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, types: { [type: string]: boolean } ): void { + private getValueCompletions(schema: SchemaService.ResolvedSchema, doc, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector): void { let offsetForSeparator = offset; let parentKey: string = null; let valueNode: Parser.ASTNode = null; @@ -258,7 +265,7 @@ export class YAMLCompletion { } if (!node) { - this.addSchemaValueCompletions(schema.schema, collector, types, ""); + this.addSchemaValueCompletions(schema.schema, collector, ""); return; } @@ -281,30 +288,21 @@ export class YAMLCompletion { if (Array.isArray(s.schema.items)) { let index = this.findItemAtOffset(node, document, offset); if (index < s.schema.items.length) { - this.addSchemaValueCompletions(s.schema.items[index], collector, types, separatorAfter); + this.addSchemaValueCompletions(s.schema.items[index], collector, separatorAfter, true); } } else { - this.addSchemaValueCompletions(s.schema.items, collector, types, separatorAfter); + this.addSchemaValueCompletions(s.schema.items, collector, separatorAfter, true); } } if (s.schema.properties) { let propertySchema = s.schema.properties[parentKey]; if (propertySchema) { - this.addSchemaValueCompletions(propertySchema, collector, types, separatorAfter); + this.addSchemaValueCompletions(propertySchema, collector, separatorAfter, false); } } } }); } - if(node){ - if (types['boolean']) { - this.addBooleanValueCompletion(true, collector, separatorAfter); - this.addBooleanValueCompletion(false, collector, separatorAfter); - } - if (types['null']) { - this.addNullValueCompletion(collector, separatorAfter); - } - } } private getContributedValueCompletions(doc: Parser.JSONDocument, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector, collectionPromises: Thenable[]) { @@ -345,22 +343,34 @@ export class YAMLCompletion { }); } - private addSchemaValueCompletions(schema: JSONSchema, collector: CompletionsCollector, types: { [type: string]: boolean }, separatorAfter: string): void { - this.addDefaultValueCompletions(schema, collector, separatorAfter); - this.addEnumValueCompletions(schema, collector, separatorAfter); + private addSchemaValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { + let types: { [type: string]: boolean } = {}; + this.addSchemaValueCompletionsCore(schema, collector, types, separatorAfter, forArrayItem); + if (types['boolean']) { + this.addBooleanValueCompletion(true, collector, separatorAfter); + this.addBooleanValueCompletion(false, collector, separatorAfter); + } + if (types['null']) { + this.addNullValueCompletion(collector, separatorAfter); + } + } + + private addSchemaValueCompletionsCore(schema: JSONSchema, collector: CompletionsCollector, types: { [type: string]: boolean }, separatorAfter: string, forArrayItem = false): void { + this.addDefaultValueCompletions(schema, collector, separatorAfter, 0, forArrayItem); + this.addEnumValueCompletions(schema, collector, separatorAfter, forArrayItem); this.collectTypes(schema, types); if (Array.isArray(schema.allOf)) { - schema.allOf.forEach(s => this.addSchemaValueCompletions(s, collector, types, separatorAfter)); + schema.allOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); } if (Array.isArray(schema.anyOf)) { - schema.anyOf.forEach(s => this.addSchemaValueCompletions(s, collector, types, separatorAfter)); + schema.anyOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); } if (Array.isArray(schema.oneOf)) { - schema.oneOf.forEach(s => this.addSchemaValueCompletions(s, collector, types, separatorAfter)); + schema.oneOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); } } - private addDefaultValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, arrayDepth = 0): void { + private addDefaultValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, arrayDepth = 0, forArrayItem = false): void { let hasProposals = false; if (schema.default) { let type = schema.type; @@ -371,8 +381,8 @@ export class YAMLCompletion { } collector.add({ kind: this.getSuggestionKind(type), - label: this.getLabelForValue(value), - insertText: this.getInsertTextForValue(value, separatorAfter), + label: forArrayItem ? `- ${this.getLabelForValue(value)}` : this.getLabelForValue(value), + insertText: forArrayItem ? `- ${this.getInsertTextForValue(value, separatorAfter)}` : this.getInsertTextForValue(value, separatorAfter), insertTextFormat: InsertTextFormat.Snippet, detail: localize('json.suggest.default', 'Default value'), }); @@ -383,7 +393,7 @@ export class YAMLCompletion { } } - private addEnumValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string): void { + private addEnumValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { if (Array.isArray(schema.enum)) { for (let i = 0, length = schema.enum.length; i < length; i++) { let enm = schema.enum[i]; @@ -393,8 +403,8 @@ export class YAMLCompletion { } collector.add({ kind: this.getSuggestionKind(schema.type), - label: this.getLabelForValue(enm), - insertText: this.getInsertTextForValue(enm, separatorAfter), + label: forArrayItem ? `- ${this.getLabelForValue(enm)}` : this.getLabelForValue(enm), + insertText: forArrayItem ? `- ${this.getInsertTextForValue(enm, separatorAfter)}` : this.getInsertTextForValue(enm, separatorAfter), insertTextFormat: InsertTextFormat.Snippet, documentation }); diff --git a/src/server.ts b/src/server.ts index 8846c608..fe686144 100755 --- a/src/server.ts +++ b/src/server.ts @@ -494,7 +494,7 @@ function completionHelper(document: TextDocument, textDocumentPosition: Position let trimmedText = textLine.trim(); if(trimmedText.length === 0 || (trimmedText.length === 1 && trimmedText[0] === '-')){ //Add a temp node that is in the document but we don't use at all. - newText = document.getText().substring(0, start+textLine.length) + "holder:\r\n" + document.getText().substr(lineOffset[linePos+1] || document.getText().length); + newText = document.getText().substring(0, start + textLine.length) + (trimmedText[0] === '-' && !textLine.endsWith(" ") ? " " : "") + "holder:\r\n" + document.getText().substr(lineOffset[linePos + 1] || document.getText().length); //For when missing semi colon case }else{ diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 4a437b34..ef6a06dc 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -199,7 +199,7 @@ function completionHelper(document: TextDocument, textDocumentPosition){ let trimmedText = textLine.trim(); if(trimmedText.length === 0 || (trimmedText.length === 1 && trimmedText[0] === '-')){ //Add a temp node that is in the document but we don't use at all. - newText = document.getText().substring(0, start+textLine.length) + "holder:\r\n" + document.getText().substr(lineOffset[linePos+1] || document.getText().length); + newText = document.getText().substring(0, start + textLine.length) + (trimmedText[0] === '-' && !textLine.endsWith(" ") ? " " : "") + "holder:\r\n" + document.getText().substr(lineOffset[linePos + 1] || document.getText().length); //For when missing semi colon case }else{ //Add a semicolon to the end of the current line so we can validate the node diff --git a/test/autoCompletion2.test.ts b/test/autoCompletion2.test.ts index 9f1e3a66..1f0ceefd 100644 --- a/test/autoCompletion2.test.ts +++ b/test/autoCompletion2.test.ts @@ -175,7 +175,7 @@ function completionHelper(document: TextDocument, textDocumentPosition){ let trimmedText = textLine.trim(); if(trimmedText.length === 0 || (trimmedText.length === 1 && trimmedText[0] === '-')){ //Add a temp node that is in the document but we don't use at all. - newText = document.getText().substring(0, start+textLine.length) + "holder:\r\n" + document.getText().substr(lineOffset[linePos+1] || document.getText().length); + newText = document.getText().substring(0, start + textLine.length) + (trimmedText[0] === '-' && !textLine.endsWith(" ") ? " " : "") + "holder:\r\n" + document.getText().substr(lineOffset[linePos + 1] || document.getText().length); //For when missing semi colon case }else{ //Add a semicolon to the end of the current line so we can validate the node diff --git a/test/autoCompletion3.test.ts b/test/autoCompletion3.test.ts new file mode 100644 index 00000000..f4ea6677 --- /dev/null +++ b/test/autoCompletion3.test.ts @@ -0,0 +1,120 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Red Hat. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { + IPCMessageReader, IPCMessageWriter, + createConnection, IConnection, TextDocumentSyncKind, + TextDocuments, TextDocument, Diagnostic, DiagnosticSeverity, + InitializeParams, InitializeResult, TextDocumentPositionParams, + CompletionItem, CompletionItemKind, RequestType +} from 'vscode-languageserver'; +import {getLanguageService} from '../src/languageservice/yamlLanguageService' +import {JSONSchemaService} from '../src/languageservice/services/jsonSchemaService' +import {schemaRequestService, workspaceContext} from './testHelper'; +import { parse as parseYAML } from '../src/languageservice/parser/yamlParser'; +import { getLineOffsets } from '../src/languageservice/utils/arrUtils'; +var assert = require('assert'); + +let languageService = getLanguageService(schemaRequestService, workspaceContext, [], null); + +let uri = 'http://json.schemastore.org/asmdef'; +let languageSettings = { + schemas: [] +}; +let fileMatch = ["*.yml", "*.yaml"]; +languageSettings.schemas.push({ uri, fileMatch: fileMatch }); +languageService.configure(languageSettings); + +suite("Auto Completion Tests", () => { + + describe('yamlCompletion with asmdef', function(){ + + describe('doComplete', function(){ + + function setup(content: string){ + return TextDocument.create("file://~/Desktop/vscode-k8s/test.yaml", "yaml", 0, content); + } + + function parseSetup(content: string, position){ + let testTextDocument = setup(content); + return completionHelper(testTextDocument, testTextDocument.positionAt(position)); + } + + it('Array of enum autocomplete without word', (done) => { + let content = "optionalUnityReferences:\n -"; + let completion = parseSetup(content, 29); + completion.then(function(result){ + assert.notEqual(result.items.length, 0); + }).then(done, done); + }); + + it('Array of enum autocomplete without word', (done) => { + let content = "optionalUnityReferences:\n - "; + let completion = parseSetup(content, 30); + completion.then(function(result){ + assert.notEqual(result.items.length, 0); + }).then(done, done); + }); + + it('Array of enum autocomplete with letter', (done) => { + let content = "optionalUnityReferences:\n - T"; + let completion = parseSetup(content, 31); + completion.then(function(result){ + assert.notEqual(result.items.length, 0); + }).then(done, done); + }); + }); + }); +}); + +function is_EOL(c) { + return (c === 0x0A/* LF */) || (c === 0x0D/* CR */); +} + +function completionHelper(document: TextDocument, textDocumentPosition){ + + //Get the string we are looking at via a substring + let linePos = textDocumentPosition.line; + let position = textDocumentPosition; + let lineOffset = getLineOffsets(document.getText()); + let start = lineOffset[linePos]; //Start of where the autocompletion is happening + let end = 0; //End of where the autocompletion is happening + if(lineOffset[linePos+1]){ + end = lineOffset[linePos+1]; + }else{ + end = document.getText().length; + } + + while (end - 1 >= 0 && is_EOL(document.getText().charCodeAt(end - 1))) { + end--; + } + + let textLine = document.getText().substring(start, end); + + //Check if the string we are looking at is a node + if(textLine.indexOf(":") === -1){ + //We need to add the ":" to load the nodes + + let newText = ""; + + //This is for the empty line case + let trimmedText = textLine.trim(); + if(trimmedText.length === 0 || (trimmedText.length === 1 && trimmedText[0] === '-')){ + //Add a temp node that is in the document but we don't use at all. + newText = document.getText().substring(0, start + textLine.length) + (trimmedText[0] === '-' && !textLine.endsWith(" ") ? " " : "") + "holder:\r\n" + document.getText().substr(lineOffset[linePos + 1] || document.getText().length); + //For when missing semi colon case + }else{ + //Add a semicolon to the end of the current line so we can validate the node + newText = document.getText().substring(0, start+textLine.length) + ":\r\n" + document.getText().substr(lineOffset[linePos+1] || document.getText().length); + } + let jsonDocument = parseYAML(newText); + return languageService.doComplete(document, position, jsonDocument); + }else{ + + //All the nodes are loaded + position.character = position.character - 1; + let jsonDocument = parseYAML(document.getText()); + return languageService.doComplete(document, position, jsonDocument); + } +} \ No newline at end of file