From 0040590b74ba66762fe2626bf6bd70ec1ba254a1 Mon Sep 17 00:00:00 2001 From: Josh Pinkney Date: Mon, 4 Nov 2019 20:47:48 -0500 Subject: [PATCH] Removed json 4 parser and updated completion to work with json 7 Signed-off-by: Josh Pinkney --- src/languageservice/jsonASTTypes.ts | 2 + src/languageservice/jsonSchema07.ts | 3 +- src/languageservice/parser/jsonParser04.ts | 1059 ----------------- src/languageservice/parser/jsonParser07.ts | 36 +- src/languageservice/parser/yamlParser04.ts | 251 ---- src/languageservice/parser/yamlParser07.ts | 2 +- .../services/yamlCompletion.ts | 490 ++++---- src/languageservice/services/yamlHover.ts | 4 +- .../services/yamlValidation.ts | 2 +- src/languageservice/utils/arrUtils.ts | 15 +- test/autoCompletion2.test.ts | 2 +- 11 files changed, 269 insertions(+), 1597 deletions(-) delete mode 100644 src/languageservice/parser/jsonParser04.ts delete mode 100644 src/languageservice/parser/yamlParser04.ts diff --git a/src/languageservice/jsonASTTypes.ts b/src/languageservice/jsonASTTypes.ts index c2c7e4fc..6a5097c2 100644 --- a/src/languageservice/jsonASTTypes.ts +++ b/src/languageservice/jsonASTTypes.ts @@ -12,6 +12,8 @@ export interface BaseASTNode { readonly length: number; readonly children?: ASTNode[]; readonly value?: string | boolean | number | null; + location: string; + getNodeFromOffsetEndInclusive(offset: number): ASTNode; } export interface ObjectASTNode extends BaseASTNode { readonly type: 'object'; diff --git a/src/languageservice/jsonSchema07.ts b/src/languageservice/jsonSchema07.ts index 5ddac961..706dc394 100644 --- a/src/languageservice/jsonSchema07.ts +++ b/src/languageservice/jsonSchema07.ts @@ -64,7 +64,8 @@ export interface JSONSchema { markdownDescription?: string; // tslint:disable-next-line: no-any body?: any; - bodyText?: string; }[]; // VSCode extension: body: a object that will be converted to a JSON string. bodyText: text with \t and \n + bodyText?: string; + }[]; // VSCode extension: body: a object that will be converted to a JSON string. bodyText: text with \t and \n errorMessage?: string; // VSCode extension patternErrorMessage?: string; // VSCode extension diff --git a/src/languageservice/parser/jsonParser04.ts b/src/languageservice/parser/jsonParser04.ts deleted file mode 100644 index 66e90c3a..00000000 --- a/src/languageservice/parser/jsonParser04.ts +++ /dev/null @@ -1,1059 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as Json from 'jsonc-parser'; -import { JSONSchema } from '../jsonSchema04'; -import * as objects from '../utils/objects'; - -import * as nls from 'vscode-nls'; -import { LanguageSettings } from '../yamlLanguageService'; -const localize = nls.loadMessageBundle(); - -export interface IRange { - start: number; - end: number; -} - -export enum ErrorCode { - Undefined = 0, - EnumValueMismatch = 1, - CommentsNotAllowed = 2 -} - -export enum ProblemSeverity { - Error, Warning -} - -export interface IProblem { - location: IRange; - severity: ProblemSeverity; - code?: ErrorCode; - message: string; -} - -export class ASTNode { - public start: number; - public end: number; - public type: string; - public parent: ASTNode; - public parserSettings: LanguageSettings; - public location: Json.Segment; - - constructor(parent: ASTNode, type: string, location: Json.Segment, start: number, end?: number) { - this.type = type; - this.location = location; - this.start = start; - this.end = end; - this.parent = parent; - this.parserSettings = { - isKubernetes: false - }; - } - - public setParserSettings(parserSettings: LanguageSettings){ - this.parserSettings = parserSettings; - } - - public getPath(): Json.JSONPath { - const path = this.parent ? this.parent.getPath() : []; - if (this.location !== null) { - path.push(this.location); - } - return path; - } - - public getChildNodes(): ASTNode[] { - return []; - } - - public getLastChild(): ASTNode { - return null; - } - -// tslint:disable-next-line: no-any - public getValue(): any { - // override in children - return; - } - - public contains(offset: number, includeRightBound: boolean = false): boolean { - return offset >= this.start && offset < this.end || includeRightBound && offset === this.end; - } - - public toString(): string { - return 'type: ' + this.type + ' (' + this.start + '/' + this.end + ')' + (this.parent ? ' parent: {' + this.parent.toString() + '}' : ''); - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - return visitor(this); - } - - public getNodeFromOffset(offset: number): ASTNode { - const findNode = (node: ASTNode): ASTNode => { - if (offset >= node.start && offset < node.end) { - const children = node.getChildNodes(); - for (let i = 0; i < children.length && children[i].start <= offset; i++) { - const item = findNode(children[i]); - if (item) { - return item; - } - } - return node; - } - return null; - }; - return findNode(this); - } - - public getNodeCollectorCount(offset: number): Number { - const collector = []; - const findNode = (node: ASTNode): ASTNode => { - const children = node.getChildNodes(); - for (let i = 0; i < children.length; i++) { - const item = findNode(children[i]); - if (item && item.type === 'property') { - collector.push(item); - } - } - return node; - }; - const foundNode = findNode(this); - return collector.length; - } - - public getNodeFromOffsetEndInclusive(offset: number): ASTNode { - const collector = []; - const findNode = (node: ASTNode): ASTNode => { - if (offset >= node.start && offset <= node.end) { - const children = node.getChildNodes(); - for (let i = 0; i < children.length && children[i].start <= offset; i++) { - const item = findNode(children[i]); - if (item) { - collector.push(item); - } - } - return node; - } - return null; - }; - const foundNode = findNode(this); - let currMinDist = Number.MAX_VALUE; - let currMinNode = null; - for (const possibleNode in collector){ - const currNode = collector[possibleNode]; - const minDist = (currNode.end - offset) + (offset - currNode.start); - if (minDist < currMinDist){ - currMinNode = currNode; - currMinDist = minDist; - } - } - return currMinNode || foundNode; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - if (Array.isArray(schema.type)) { - if ((schema.type).indexOf(this.type) === -1) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('typeArrayMismatchWarning', 'Incorrect type. Expected one of {0}.', (schema.type).join(', ')) - }); - } - } - else if (schema.type) { - if (this.type !== schema.type) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('typeMismatchWarning', 'Incorrect type. Expected "{0}".', schema.type) - }); - } - } - if (Array.isArray(schema.allOf)) { - schema.allOf.forEach(subSchema => { - this.validate(subSchema, validationResult, matchingSchemas); - }); - } - if (schema.not) { - const subValidationResult = new ValidationResult(); - const subMatchingSchemas = matchingSchemas.newSub(); - this.validate(schema.not, subValidationResult, subMatchingSchemas); - if (!subValidationResult.hasProblems()) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('notSchemaWarning', 'Matches a schema that is not allowed.') - }); - } - subMatchingSchemas.schemas.forEach(ms => { - ms.inverted = !ms.inverted; - matchingSchemas.add(ms); - }); - } - - const testAlternatives = (alternatives: JSONSchema[], maxOneMatch: boolean) => { - const matches = []; - - // remember the best match that is used for error messages - let bestMatch: { schema: JSONSchema; validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } = null; - alternatives.forEach(subSchema => { - const subValidationResult = new ValidationResult(); - const subMatchingSchemas = matchingSchemas.newSub(); - - this.validate(subSchema, subValidationResult, subMatchingSchemas); - if (!subValidationResult.hasProblems()) { - matches.push(subSchema); - } - if (!bestMatch) { - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if (this.parserSettings.isKubernetes) { - bestMatch = alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas); - } else { - bestMatch = genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas); - } - }); - - if (matches.length > 1 && maxOneMatch && !this.parserSettings.isKubernetes) { - validationResult.problems.push({ - location: { start: this.start, end: this.start + 1 }, - severity: ProblemSeverity.Warning, - message: localize('oneOfWarning', 'Matches multiple schemas when only one must validate.') - }); - } - if (bestMatch !== null) { - validationResult.merge(bestMatch.validationResult); - validationResult.propertiesMatches += bestMatch.validationResult.propertiesMatches; - validationResult.propertiesValueMatches += bestMatch.validationResult.propertiesValueMatches; - matchingSchemas.merge(bestMatch.matchingSchemas); - } - return matches.length; - }; - if (Array.isArray(schema.anyOf)) { - testAlternatives(schema.anyOf, false); - } - if (Array.isArray(schema.oneOf)) { - testAlternatives(schema.oneOf, true); - } - - if (Array.isArray(schema.enum)) { - const val = this.getValue(); - let enumValueMatch = false; - for (const e of schema.enum) { - if (objects.equals(val, e)) { - enumValueMatch = true; - break; - } - } - validationResult.enumValues = schema.enum; - validationResult.enumValueMatch = enumValueMatch; - if (!enumValueMatch) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - code: ErrorCode.EnumValueMismatch, - message: schema.errorMessage || localize('enumWarning', 'Value is not accepted. Valid values: {0}.', schema.enum.map(v => JSON.stringify(v)).join(', ')) - }); - } - } - - if (schema.deprecationMessage && this.parent) { - validationResult.problems.push({ - location: { start: this.parent.start, end: this.parent.end }, - severity: ProblemSeverity.Warning, - message: schema.deprecationMessage - }); - } - matchingSchemas.add({ node: this, schema: schema }); - } -} - -export class NullASTNode extends ASTNode { - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'null', name, start, end); - } - -// tslint:disable-next-line: no-any - public getValue(): any { - return null; - } -} - -export class BooleanASTNode extends ASTNode { - - private value: boolean | string; - - constructor(parent: ASTNode, name: Json.Segment, value: boolean | string, start: number, end?: number) { - super(parent, 'boolean', name, start, end); - this.value = value; - } - -// tslint:disable-next-line: no-any - public getValue(): any { - return this.value; - } - -} - -export class ArrayASTNode extends ASTNode { - - public items: ASTNode[]; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'array', name, start, end); - this.items = []; - } - - public getChildNodes(): ASTNode[] { - return this.items; - } - - public getLastChild(): ASTNode { - return this.items[this.items.length - 1]; - } - -// tslint:disable-next-line: no-any - public getValue(): any { - return this.items.map(v => v.getValue()); - } - - public addItem(item: ASTNode): boolean { - if (item) { - this.items.push(item); - return true; - } - return false; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - let ctn = visitor(this); - for (let i = 0; i < this.items.length && ctn; i++) { - ctn = this.items[i].visit(visitor); - } - return ctn; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - super.validate(schema, validationResult, matchingSchemas); - - if (Array.isArray(schema.items)) { - const subSchemas = schema.items; - subSchemas.forEach((subSchema, index) => { - const itemValidationResult = new ValidationResult(); - const item = this.items[index]; - if (item) { - item.validate(subSchema, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - } else if (this.items.length >= subSchemas.length) { - validationResult.propertiesValueMatches++; - } - }); - if (this.items.length > subSchemas.length) { - if (typeof schema.additionalItems === 'object') { - for (let i = subSchemas.length; i < this.items.length; i++) { - const itemValidationResult = new ValidationResult(); -// tslint:disable-next-line: no-any - this.items[i].validate(schema.additionalItems, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - } - } else if (schema.additionalItems === false) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('additionalItemsWarning', 'Array has too many items according to schema. Expected {0} or fewer.', subSchemas.length) - }); - } - } - } - else if (schema.items) { - this.items.forEach(item => { - const itemValidationResult = new ValidationResult(); - item.validate(schema.items, itemValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(itemValidationResult); - }); - } - - if (schema.minItems && this.items.length < schema.minItems) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minItemsWarning', 'Array has too few items. Expected {0} or more.', schema.minItems) - }); - } - - if (schema.maxItems && this.items.length > schema.maxItems) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maxItemsWarning', 'Array has too many items. Expected {0} or fewer.', schema.minItems) - }); - } - - if (schema.uniqueItems === true) { - const values = this.items.map(node => - node.getValue()); - const duplicates = values.some((value, index) => - index !== values.lastIndexOf(value)); - if (duplicates) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('uniqueItemsWarning', 'Array has duplicate items.') - }); - } - } - } -} - -export class NumberASTNode extends ASTNode { - - public isInteger: boolean; - public value: number; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'number', name, start, end); - this.isInteger = true; - this.value = Number.NaN; - } - -// tslint:disable-next-line: no-any - public getValue(): any { - return this.value; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - // work around type validation in the base class - let typeIsInteger = false; - if (schema.type === 'integer' || (Array.isArray(schema.type) && (schema.type).indexOf('integer') !== -1)) { - typeIsInteger = true; - } - if (typeIsInteger && this.isInteger === true) { - this.type = 'integer'; - } - super.validate(schema, validationResult, matchingSchemas); - this.type = 'number'; - - const val = this.getValue(); - - if (typeof schema.multipleOf === 'number') { - if (val % schema.multipleOf !== 0) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('multipleOfWarning', 'Value is not divisible by {0}.', schema.multipleOf) - }); - } - } - - if (typeof schema.minimum === 'number') { - if (schema.exclusiveMinimum && val <= schema.minimum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('exclusiveMinimumWarning', 'Value is below the exclusive minimum of {0}.', schema.minimum) - }); - } - if (!schema.exclusiveMinimum && val < schema.minimum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minimumWarning', 'Value is below the minimum of {0}.', schema.minimum) - }); - } - } - - if (typeof schema.maximum === 'number') { - if (schema.exclusiveMaximum && val >= schema.maximum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('exclusiveMaximumWarning', 'Value is above the exclusive maximum of {0}.', schema.maximum) - }); - } - if (!schema.exclusiveMaximum && val > schema.maximum) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maximumWarning', 'Value is above the maximum of {0}.', schema.maximum) - }); - } - } - - } -} - -export class StringASTNode extends ASTNode { - public isKey: boolean; - public value: string; - - constructor(parent: ASTNode, name: Json.Segment, isKey: boolean, start: number, end?: number) { - super(parent, 'string', name, start, end); - this.isKey = isKey; - this.value = ''; - } - -// tslint:disable-next-line: no-any - public getValue(): any { - return this.value; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - super.validate(schema, validationResult, matchingSchemas); - - if (schema.minLength && this.value.length < schema.minLength) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('minLengthWarning', 'String is shorter than the minimum length of {0}.', schema.minLength) - }); - } - - if (schema.maxLength && this.value.length > schema.maxLength) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('maxLengthWarning', 'String is longer than the maximum length of {0}.', schema.maxLength) - }); - } - - if (schema.pattern) { - const regex = new RegExp(schema.pattern); - if (!regex.test(this.value)) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: schema.patternErrorMessage || schema.errorMessage || localize('patternWarning', 'String does not match the pattern of "{0}".', schema.pattern) - }); - } - } - - } -} - -export class PropertyASTNode extends ASTNode { - public key: StringASTNode; - public value: ASTNode; - public colonOffset: number; - - constructor(parent: ASTNode, key: StringASTNode) { - super(parent, 'property', null, key.start); - this.key = key; - key.parent = this; - key.location = key.value; - this.colonOffset = -1; - } - - public getChildNodes(): ASTNode[] { - return this.value ? [this.key, this.value] : [this.key]; - } - - public getLastChild(): ASTNode { - return this.value; - } - - public setValue(value: ASTNode): boolean { - this.value = value; - return value !== null; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - return visitor(this) && this.key.visit(visitor) && this.value && this.value.visit(visitor); - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - if (this.value) { - this.value.validate(schema, validationResult, matchingSchemas); - } - } -} - -export class ObjectASTNode extends ASTNode { - public properties: PropertyASTNode[]; - - constructor(parent: ASTNode, name: Json.Segment, start: number, end?: number) { - super(parent, 'object', name, start, end); - - this.properties = []; - } - - public getChildNodes(): ASTNode[] { - return this.properties; - } - - public getLastChild(): ASTNode { - return this.properties[this.properties.length - 1]; - } - - public addProperty(node: PropertyASTNode): boolean { - if (!node) { - return false; - } - this.properties.push(node); - return true; - } - - public getFirstProperty(key: string): PropertyASTNode { - for (let i = 0; i < this.properties.length; i++) { - if (this.properties[i].key.value === key) { - return this.properties[i]; - } - } - return null; - } - - public getKeyList(): string[] { - return this.properties.map(p => p.key.getValue()); - } - -// tslint:disable-next-line: no-any - public getValue(): any { -// tslint:disable-next-line: no-any - const value: any = Object.create(null); - this.properties.forEach(p => { - const v = p.value && p.value.getValue(); - if (typeof v !== 'undefined') { - value[p.key.getValue()] = v; - } - }); - return value; - } - - public visit(visitor: (node: ASTNode) => boolean): boolean { - let ctn = visitor(this); - for (let i = 0; i < this.properties.length && ctn; i++) { - ctn = this.properties[i].visit(visitor); - } - return ctn; - } - - public validate(schema: JSONSchema, validationResult: ValidationResult, matchingSchemas: ISchemaCollector): void { - if (!matchingSchemas.include(this)) { - return; - } - - super.validate(schema, validationResult, matchingSchemas); - const seenKeys: { [key: string]: ASTNode } = Object.create(null); - const unprocessedProperties: string[] = []; - this.properties.forEach(node => { - - const key = node.key.value; - - //Replace the merge key with the actual values of what the node value points to in seen keys - if (key === '<<' && node.value) { - - switch (node.value.type) { - case 'object': { - node.value['properties'].forEach(propASTNode => { - const propKey = propASTNode.key.value; - seenKeys[propKey] = propASTNode.value; - unprocessedProperties.push(propKey); - }); - break; - } - case 'array': { - node.value['items'].forEach(sequenceNode => { - sequenceNode['properties'].forEach(propASTNode => { - const seqKey = propASTNode.key.value; - seenKeys[seqKey] = propASTNode.value; - unprocessedProperties.push(seqKey); - }); - }); - break; - } - default: { - break; - } - } - }else{ - seenKeys[key] = node.value; - unprocessedProperties.push(key); - } - - }); - - if (Array.isArray(schema.required)) { - schema.required.forEach((propertyName: string) => { - if (!seenKeys[propertyName]) { - const key = this.parent && this.parent && (this.parent).key; - const location = key ? { start: key.start, end: key.end } : { start: this.start, end: this.start + 1 }; - validationResult.problems.push({ - location: location, - severity: ProblemSeverity.Warning, - message: localize('MissingRequiredPropWarning', 'Missing property "{0}".', propertyName) - }); - } - }); - } - - const propertyProcessed = (prop: string) => { - let index = unprocessedProperties.indexOf(prop); - while (index >= 0) { - unprocessedProperties.splice(index, 1); - index = unprocessedProperties.indexOf(prop); - } - }; - - if (schema.properties) { - Object.keys(schema.properties).forEach((propertyName: string) => { - propertyProcessed(propertyName); - const prop = schema.properties[propertyName]; - const child = seenKeys[propertyName]; - if (child) { - const propertyValidationResult = new ValidationResult(); - child.validate(prop, propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - - }); - } - - if (schema.patternProperties) { - Object.keys(schema.patternProperties).forEach((propertyPattern: string) => { - const regex = new RegExp(propertyPattern); - unprocessedProperties.slice(0).forEach((propertyName: string) => { - if (regex.test(propertyName)) { - propertyProcessed(propertyName); - const child = seenKeys[propertyName]; - if (child) { - const propertyValidationResult = new ValidationResult(); - child.validate(schema.patternProperties[propertyPattern], propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - - } - }); - }); - } - - if (typeof schema.additionalProperties === 'object') { - unprocessedProperties.forEach((propertyName: string) => { - const child = seenKeys[propertyName]; - if (child) { - const propertyValidationResult = new ValidationResult(); -// tslint:disable-next-line: no-any - child.validate(schema.additionalProperties, propertyValidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyValidationResult); - } - }); - } else if (schema.additionalProperties === false) { - if (unprocessedProperties.length > 0) { - unprocessedProperties.forEach((propertyName: string) => { - const child = seenKeys[propertyName]; - if (child) { - let propertyNode = null; - if (child.type !== 'property'){ - propertyNode = child.parent; - if (propertyNode.type === 'object'){ - propertyNode = propertyNode.properties[0]; - } - }else{ - propertyNode = child; - } - validationResult.problems.push({ - location: { start: propertyNode.key.start, end: propertyNode.key.end }, - severity: ProblemSeverity.Warning, - message: schema.errorMessage || localize('DisallowedExtraPropWarning', 'Unexpected property {0}', propertyName) - }); - } - }); - } - } - - if (schema.maxProperties) { - if (this.properties.length > schema.maxProperties) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('MaxPropWarning', 'Object has more properties than limit of {0}.', schema.maxProperties) - }); - } - } - - if (schema.minProperties) { - if (this.properties.length < schema.minProperties) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('MinPropWarning', 'Object has fewer properties than the required number of {0}', schema.minProperties) - }); - } - } - - if (schema.dependencies) { - Object.keys(schema.dependencies).forEach((key: string) => { - const prop = seenKeys[key]; - if (prop) { - const propertyDep = schema.dependencies[key]; - if (Array.isArray(propertyDep)) { - propertyDep.forEach((requiredProp: string) => { - if (!seenKeys[requiredProp]) { - validationResult.problems.push({ - location: { start: this.start, end: this.end }, - severity: ProblemSeverity.Warning, - message: localize('RequiredDependentPropWarning', 'Object is missing property {0} required by property {1}.', requiredProp, key) - }); - } else { - validationResult.propertiesValueMatches++; - } - }); - } else if (propertyDep) { - const propertyvalidationResult = new ValidationResult(); - this.validate(propertyDep, propertyvalidationResult, matchingSchemas); - validationResult.mergePropertyMatch(propertyvalidationResult); - } - } - }); - } - } -} - -export interface IApplicableSchema { - node: ASTNode; - inverted?: boolean; - schema: JSONSchema; -} - -export enum EnumMatch { - Key, Enum -} - -export interface ISchemaCollector { - schemas: IApplicableSchema[]; - add(schema: IApplicableSchema): void; - merge(other: ISchemaCollector): void; - include(node: ASTNode): boolean; - newSub(): ISchemaCollector; -} - -class SchemaCollector implements ISchemaCollector { - schemas: IApplicableSchema[] = []; - constructor(private focusOffset = -1, private exclude: ASTNode = null) { - } - add(schema: IApplicableSchema) { - this.schemas.push(schema); - } - merge(other: ISchemaCollector) { - this.schemas.push(...other.schemas); - } - include(node: ASTNode) { - return (this.focusOffset === -1 || node.contains(this.focusOffset)) && (node !== this.exclude); - } - newSub(): ISchemaCollector { - return new SchemaCollector(-1, this.exclude); - } -} - -class NoOpSchemaCollector implements ISchemaCollector { - get schemas() { return []; } - add(schema: IApplicableSchema) { } - merge(other: ISchemaCollector) { } - include(node: ASTNode) { return true; } - newSub(): ISchemaCollector { return this; } -} - -export class ValidationResult { - public problems: IProblem[]; - - public propertiesMatches: number; - public propertiesValueMatches: number; - public primaryValueMatches: number; - public enumValueMatch: boolean; -// tslint:disable-next-line: no-any - public enumValues: any[]; - public warnings; - public errors; - - constructor() { - this.problems = []; - this.propertiesMatches = 0; - this.propertiesValueMatches = 0; - this.primaryValueMatches = 0; - this.enumValueMatch = false; - this.enumValues = null; - this.warnings = []; - this.errors = []; - } - - public hasProblems(): boolean { - return !!this.problems.length; - } - - public mergeAll(validationResults: ValidationResult[]): void { - validationResults.forEach(validationResult => { - this.merge(validationResult); - }); - } - - public merge(validationResult: ValidationResult): void { - this.problems = this.problems.concat(validationResult.problems); - } - - public mergeEnumValues(validationResult: ValidationResult): void { - if (!this.enumValueMatch && !validationResult.enumValueMatch && this.enumValues && validationResult.enumValues) { - this.enumValues = this.enumValues.concat(validationResult.enumValues); - for (const error of this.problems) { - if (error.code === ErrorCode.EnumValueMismatch) { - error.message = localize('enumWarning', 'Value is not accepted. Valid values: {0}.', this.enumValues.map(v => JSON.stringify(v)).join(', ')); - } - } - } - } - - public mergePropertyMatch(propertyValidationResult: ValidationResult): void { - this.merge(propertyValidationResult); - this.propertiesMatches++; - if (propertyValidationResult.enumValueMatch || !this.hasProblems() && propertyValidationResult.propertiesMatches) { - this.propertiesValueMatches++; - } - if (propertyValidationResult.enumValueMatch && propertyValidationResult.enumValues) { - this.primaryValueMatches++; - } - } - - public compareGeneric(other: ValidationResult): number { - const hasProblems = this.hasProblems(); - if (hasProblems !== other.hasProblems()) { - return hasProblems ? -1 : 1; - } - if (this.enumValueMatch !== other.enumValueMatch) { - return other.enumValueMatch ? -1 : 1; - } - if (this.propertiesValueMatches !== other.propertiesValueMatches) { - return this.propertiesValueMatches - other.propertiesValueMatches; - } - if (this.primaryValueMatches !== other.primaryValueMatches) { - return this.primaryValueMatches - other.primaryValueMatches; - } - return this.propertiesMatches - other.propertiesMatches; - } - - public compareKubernetes(other: ValidationResult): number { - const hasProblems = this.hasProblems(); - if (this.propertiesMatches !== other.propertiesMatches) { - return this.propertiesMatches - other.propertiesMatches; - } - if (this.enumValueMatch !== other.enumValueMatch) { - return other.enumValueMatch ? -1 : 1; - } - if (this.primaryValueMatches !== other.primaryValueMatches) { - return this.primaryValueMatches - other.primaryValueMatches; - } - if (this.propertiesValueMatches !== other.propertiesValueMatches) { - return this.propertiesValueMatches - other.propertiesValueMatches; - } - if (hasProblems !== other.hasProblems()) { - return hasProblems ? -1 : 1; - } - return this.propertiesMatches - other.propertiesMatches; - } - -} - -export class JSONDocument { - - constructor(public readonly root: ASTNode, public readonly syntaxErrors: IProblem[]) { - } - - public getNodeFromOffset(offset: number): ASTNode { - return this.root && this.root.getNodeFromOffset(offset); - } - - public getNodeFromOffsetEndInclusive(offset: number): ASTNode { - return this.root && this.root.getNodeFromOffsetEndInclusive(offset); - } - - public visit(visitor: (node: ASTNode) => boolean): void { - if (this.root) { - this.root.visit(visitor); - } - } - - public configureSettings(parserSettings: LanguageSettings){ - if (this.root) { - this.root.setParserSettings(parserSettings); - } - } - - public validate(schema: JSONSchema): IProblem[] { - if (this.root && schema) { - const validationResult = new ValidationResult(); - this.root.validate(schema, validationResult, new NoOpSchemaCollector()); - return validationResult.problems; - } - return null; - } - - public getMatchingSchemas(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null): IApplicableSchema[] { - const matchingSchemas = new SchemaCollector(focusOffset, exclude); - const validationResult = new ValidationResult(); - if (this.root && schema) { - this.root.validate(schema, validationResult, matchingSchemas); - } - return matchingSchemas.schemas; - } - - public getValidationProblems(schema: JSONSchema, focusOffset: number = -1, exclude: ASTNode = null) { - const matchingSchemas = new SchemaCollector(focusOffset, exclude); - const validationResult = new ValidationResult(); - if (this.root && schema) { - this.root.validate(schema, validationResult, matchingSchemas); - } - return validationResult.problems; - } -} - -//Alternative comparison is specifically used by the kubernetes/openshift schema but may lead to better results then genericComparison depending on the schema -function alternativeComparison(subValidationResult, bestMatch, subSchema, subMatchingSchemas){ - const compareResult = subValidationResult.compareKubernetes(bestMatch.validationResult); - if (compareResult > 0) { - // our node is the best matching so far - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if (compareResult === 0) { - // there's already a best matching but we are as good - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.mergeEnumValues(subValidationResult); - } - return bestMatch; -} - -//genericComparison tries to find the best matching schema using a generic comparison -function genericComparison(maxOneMatch, subValidationResult, bestMatch, subSchema, subMatchingSchemas){ - if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { - // no errors, both are equally good matches - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; - bestMatch.validationResult.propertiesValueMatches += subValidationResult.propertiesValueMatches; - } else { - const compareResult = subValidationResult.compareGeneric(bestMatch.validationResult); - if (compareResult > 0) { - // our node is the best matching so far - bestMatch = { schema: subSchema, validationResult: subValidationResult, matchingSchemas: subMatchingSchemas }; - } else if (compareResult === 0) { - // there's already a best matching but we are as good - bestMatch.matchingSchemas.merge(subMatchingSchemas); - bestMatch.validationResult.mergeEnumValues(subValidationResult); - } - } - return bestMatch; -} diff --git a/src/languageservice/parser/jsonParser07.ts b/src/languageservice/parser/jsonParser07.ts index e0673717..2e364dbb 100644 --- a/src/languageservice/parser/jsonParser07.ts +++ b/src/languageservice/parser/jsonParser07.ts @@ -7,7 +7,7 @@ import * as Json from 'jsonc-parser'; import { JSONSchema, JSONSchemaRef } from '../jsonSchema07'; import { isNumber, equals, isString, isDefined, isBoolean } from '../utils/objects'; -import { ASTNode, ObjectASTNode, ArrayASTNode, BooleanASTNode, NumberASTNode, StringASTNode, NullASTNode, PropertyASTNode } from '../jsonASTTypes'; +import { ASTNode, ObjectASTNode, ArrayASTNode, BooleanASTNode, NumberASTNode, StringASTNode, NullASTNode, PropertyASTNode, BaseASTNode } from '../jsonASTTypes'; import { ErrorCode, JSONPath } from 'vscode-json-languageservice'; import * as nls from 'vscode-nls'; import { URI } from 'vscode-uri'; @@ -37,6 +37,7 @@ export abstract class ASTNodeImpl { public offset: number; public length: number; public readonly parent: ASTNode; + public location: string; constructor(parent: ASTNode, offset: number, length?: number) { this.offset = offset; @@ -44,6 +45,35 @@ export abstract class ASTNodeImpl { this.parent = parent; } + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + const collector = []; + const findNode = (node: ASTNode | ASTNodeImpl): ASTNode | ASTNodeImpl => { + if (offset >= node.offset && offset <= (node.offset + node.length)) { + const children = node.children; + for (let i = 0; i < children.length && children[i].offset <= offset; i++) { + const item = findNode(children[i]); + if (item) { + collector.push(item); + } + } + return node; + } + return null; + }; + const foundNode = findNode(this); + let currMinDist = Number.MAX_VALUE; + let currMinNode = null; + for (const possibleNode in collector){ + const currNode = collector[possibleNode]; + const minDist = ((currNode.length + currNode.offset) - offset) + (offset - currNode.offset); + if (minDist < currMinDist){ + currMinNode = currNode; + currMinDist = minDist; + } + } + return currMinNode || foundNode; + } + public get children(): ASTNode[] { return []; } @@ -330,6 +360,10 @@ export class JSONDocument { return void 0; } + public getNodeFromOffsetEndInclusive(offset: number): ASTNode { + return this.root && this.root.getNodeFromOffsetEndInclusive(offset); + } + public visit(visitor: (node: ASTNode) => boolean): void { if (this.root) { const doVisit = (node: ASTNode): boolean => { diff --git a/src/languageservice/parser/yamlParser04.ts b/src/languageservice/parser/yamlParser04.ts deleted file mode 100644 index 75f2decc..00000000 --- a/src/languageservice/parser/yamlParser04.ts +++ /dev/null @@ -1,251 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Copyright (c) Adam Voss. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { ASTNode, ErrorCode, BooleanASTNode, NullASTNode, ArrayASTNode, NumberASTNode, ObjectASTNode, PropertyASTNode, StringASTNode, JSONDocument } from './jsonParser04'; - -import * as nls from 'vscode-nls'; -const localize = nls.loadMessageBundle(); - -import * as Yaml from 'yaml-ast-parser-custom-tags'; -import { Schema, Type } from 'js-yaml'; - -import { getLineStartPositions, getPosition } from '../utils/documentPositionCalculator'; -import { parseYamlBoolean } from './scalar-type'; -import { filterInvalidCustomTags } from '../utils/arrUtils'; - -export class SingleYAMLDocument extends JSONDocument { - private lines; - public root; - public errors; - public warnings; - - constructor(lines: number[]) { - super(null, []); - this.lines = lines; - this.root = null; - this.errors = []; - this.warnings = []; - } - - public getSchemas(schema, doc, node) { - const matchingSchemas = []; - doc.validate(schema, matchingSchemas, node.start); - return matchingSchemas; - } - - public getNodeFromOffset(offset: number): ASTNode { - return this.getNodeFromOffsetEndInclusive(offset); - } - -} - -function recursivelyBuildAst(parent: ASTNode, node: Yaml.YAMLNode): ASTNode { - - if (!node) { - return; - } - - switch (node.kind) { - case Yaml.Kind.MAP: { - const instance = node; - - const result = new ObjectASTNode(parent, null, node.startPosition, node.endPosition); - - for (const mapping of instance.mappings) { - result.addProperty(recursivelyBuildAst(result, mapping)); - } - - return result; - } - case Yaml.Kind.MAPPING: { - const instance = node; - const key = instance.key; - - // Technically, this is an arbitrary node in YAML - // I doubt we would get a better string representation by parsing it - const keyNode = new StringASTNode(null, null, true, key.startPosition, key.endPosition); - keyNode.value = key.value; - - const result = new PropertyASTNode(parent, keyNode); - result.end = instance.endPosition; - - const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNode(parent, key.value, instance.endPosition, instance.endPosition); - valueNode.location = key.value; - - result.setValue(valueNode); - - return result; - } - case Yaml.Kind.SEQ: { - const instance = node; - - const result = new ArrayASTNode(parent, null, instance.startPosition, instance.endPosition); - - let count = 0; - for (const item of instance.items) { - if (item === null && count === instance.items.length - 1) { - break; - } - - // Be aware of https://github.com/nodeca/js-yaml/issues/321 - // Cannot simply work around it here because we need to know if we are in Flow or Block - const itemNode = (item === null) ? new NullASTNode(parent, null, instance.endPosition, instance.endPosition) : recursivelyBuildAst(result, item); - - itemNode.location = count++; - result.addItem(itemNode); - } - - return result; - } - case Yaml.Kind.SCALAR: { - const instance = node; - const type = Yaml.determineScalarType(instance); - - // The name is set either by the sequence or the mapping case. - const name = null; - const value = instance.value; - - //This is a patch for redirecting values with these strings to be boolean nodes because its not supported in the parser. - const possibleBooleanValues = ['y', 'Y', 'yes', 'Yes', 'YES', 'n', 'N', 'no', 'No', 'NO', 'on', 'On', 'ON', 'off', 'Off', 'OFF']; - if (instance.plainScalar && possibleBooleanValues.indexOf(value.toString()) !== -1) { - return new BooleanASTNode(parent, name, parseYamlBoolean(value), node.startPosition, node.endPosition); - } - - switch (type) { - case Yaml.ScalarType.null: { - return new StringASTNode(parent, name, false, instance.startPosition, instance.endPosition); - } - case Yaml.ScalarType.bool: { - return new BooleanASTNode(parent, name, Yaml.parseYamlBoolean(value), node.startPosition, node.endPosition); - } - case Yaml.ScalarType.int: { - const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); - result.value = Yaml.parseYamlInteger(value); - result.isInteger = true; - return result; - } - case Yaml.ScalarType.float: { - const result = new NumberASTNode(parent, name, node.startPosition, node.endPosition); - result.value = Yaml.parseYamlFloat(value); - result.isInteger = false; - return result; - } - case Yaml.ScalarType.string: { - const result = new StringASTNode(parent, name, false, node.startPosition, node.endPosition); - result.value = node.value; - return result; - } - } - - break; - } - case Yaml.Kind.ANCHOR_REF: { - const instance = (node).value; - - return recursivelyBuildAst(parent, instance) || - new NullASTNode(parent, null, node.startPosition, node.endPosition); - } - case Yaml.Kind.INCLUDE_REF: { - const result = new StringASTNode(parent, null, false, node.startPosition, node.endPosition); - result.value = node.value; - return result; - } - } -} - -function convertError(e: Yaml.YAMLException) { - return { message: `${e.reason}`, location: { start: e.mark.position, end: e.mark.position + e.mark.column, code: ErrorCode.Undefined } }; -} - -function createJSONDocument(yamlDoc: Yaml.YAMLNode, startPositions: number[], text: string) { - const _doc = new SingleYAMLDocument(startPositions); - _doc.root = recursivelyBuildAst(null, yamlDoc); - - if (!_doc.root) { - // TODO: When this is true, consider not pushing the other errors. - _doc.errors.push({ message: localize('Invalid symbol', 'Expected a YAML object, array or literal'), - code: ErrorCode.Undefined, - location: { start: yamlDoc.startPosition, end: yamlDoc.endPosition } }); - } - - const duplicateKeyReason = 'duplicate key'; - - //Patch ontop of yaml-ast-parser to disable duplicate key message on merge key - const isDuplicateAndNotMergeKey = function (error: Yaml.YAMLException, yamlText: string) { - const errorConverted = convertError(error); - const errorStart = errorConverted.location.start; - const errorEnd = errorConverted.location.end; - if (error.reason === duplicateKeyReason && yamlText.substring(errorStart, errorEnd).startsWith('<<')) { - return false; - } - return true; - }; - const errors = yamlDoc.errors.filter(e => e.reason !== duplicateKeyReason && !e.isWarning).map(e => convertError(e)); - const warnings = yamlDoc.errors.filter(e => (e.reason === duplicateKeyReason && isDuplicateAndNotMergeKey(e, text)) || e.isWarning).map(e => convertError(e)); - - errors.forEach(e => _doc.errors.push(e)); - warnings.forEach(e => _doc.warnings.push(e)); - - return _doc; -} - -export class YAMLDocument { - public documents: JSONDocument[]; - private errors; - private warnings; - - constructor(documents: JSONDocument[]) { - this.documents = documents; - this.errors = []; - this.warnings = []; - } - -} - -export function parse(text: string, customTags = []): YAMLDocument { - - const startPositions = getLineStartPositions(text); - // This is documented to return a YAMLNode even though the - // typing only returns a YAMLDocument - const yamlDocs = []; - - const filteredTags = filterInvalidCustomTags(customTags); - - const schemaWithAdditionalTags = Schema.create(filteredTags.map(tag => { - const typeInfo = tag.split(' '); - return new Type(typeInfo[0], { kind: (typeInfo[1] && typeInfo[1].toLowerCase()) || 'scalar' }); - })); - - /** - * Collect the additional tags into a map of string to possible tag types - */ - const tagWithAdditionalItems = new Map(); - filteredTags.forEach(tag => { - const typeInfo = tag.split(' '); - const tagName = typeInfo[0]; - const tagType = (typeInfo[1] && typeInfo[1].toLowerCase()) || 'scalar'; - if (tagWithAdditionalItems.has(tagName)) { - tagWithAdditionalItems.set(tagName, tagWithAdditionalItems.get(tagName).concat([tagType])); - } else { - tagWithAdditionalItems.set(tagName, [tagType]); - } - }); - - tagWithAdditionalItems.forEach((additionalTagKinds, key) => { - const newTagType = new Type(key, { kind: additionalTagKinds[0] || 'scalar' }); - newTagType.additionalKinds = additionalTagKinds; - schemaWithAdditionalTags.compiledTypeMap[key] = newTagType; - }); - - const additionalOptions: Yaml.LoadOptions = { - schema: schemaWithAdditionalTags - }; - - Yaml.loadAll(text, doc => yamlDocs.push(doc), additionalOptions); - - return new YAMLDocument(yamlDocs.map(doc => createJSONDocument(doc, startPositions, text))); -} diff --git a/src/languageservice/parser/yamlParser07.ts b/src/languageservice/parser/yamlParser07.ts index 6e4179aa..049d2eca 100644 --- a/src/languageservice/parser/yamlParser07.ts +++ b/src/languageservice/parser/yamlParser07.ts @@ -73,7 +73,7 @@ function recursivelyBuildAst(parent: ASTNode, node: Yaml.YAMLNode): ASTNode { keyNode.value = key.value; const valueNode = (instance.value) ? recursivelyBuildAst(result, instance.value) : new NullASTNodeImpl(parent, instance.endPosition, 0); - //valueNode.location = key.value; + valueNode.location = key.value; result.keyNode = keyNode; result.valueNode = valueNode; diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index da5b00ab..0e4bb43c 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -5,28 +5,35 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as Parser from '../parser/jsonParser04'; -import { parse as parseYAML } from '../parser/yamlParser04'; -import * as Json from 'jsonc-parser'; +import * as Parser from '../parser/jsonParser07'; +import { ASTNode, ObjectASTNode, PropertyASTNode } from '../jsonASTTypes'; +import { parse as parseYAML } from '../parser/yamlParser07'; import { YAMLSchemaService } from './yamlSchemaService'; -import { JSONSchema } from '../jsonSchema04'; +import { JSONSchema, JSONSchemaRef } from '../jsonSchema07'; import { PromiseConstructor, Thenable, JSONWorkerContribution, CompletionsCollector } from 'vscode-json-languageservice'; -import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, Position, Range, TextEdit, InsertTextFormat } from 'vscode-languageserver-types'; +import { CompletionItem, CompletionItemKind, CompletionList, TextDocument, + Position, Range, TextEdit, InsertTextFormat, MarkupContent, MarkupKind } from 'vscode-languageserver-types'; import * as nls from 'vscode-nls'; -import { getLineOffsets, matchOffsetToDocument, filterInvalidCustomTags } from '../utils/arrUtils'; +import { getLineOffsets, filterInvalidCustomTags, matchOffsetToDocument } from '../utils/arrUtils'; 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'; const localize = nls.loadMessageBundle(); -export class YAMLCompletion { +export class YAMLCompletion extends JSONCompletion { private schemaService: YAMLSchemaService; private contributions: JSONWorkerContribution[]; private promise: PromiseConstructor; private customTags: Array; private completion: boolean; + private supportsMarkdown: boolean | undefined; - constructor(schemaService: YAMLSchemaService, contributions: JSONWorkerContribution[] = [], promiseConstructor?: PromiseConstructor) { + constructor(schemaService: YAMLSchemaService, contributions: JSONWorkerContribution[] = [], + promiseConstructor: PromiseConstructor = Promise, private clientCapabilities: ClientCapabilities = {}) { + super(schemaService, contributions, promiseConstructor); this.schemaService = schemaService; this.contributions = contributions; this.promise = promiseConstructor || Promise; @@ -83,17 +90,17 @@ export class YAMLCompletion { // return Promise.resolve(result); // } - const currentWord = this.getCurrentWord(document, offset); + const currentWord = super.getCurrentWord(document, offset); let overwriteRange = null; if (node && node.type === 'null') { - const nodeStartPos = document.positionAt(node.start); + const nodeStartPos = document.positionAt(node.offset); nodeStartPos.character += 1; - const nodeEndPos = document.positionAt(node.end); + const nodeEndPos = document.positionAt(node.offset + node.length); nodeEndPos.character += 1; overwriteRange = Range.create(nodeStartPos, nodeEndPos); }else if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean')) { - overwriteRange = Range.create(document.positionAt(node.start), document.positionAt(node.end)); + overwriteRange = Range.create(document.positionAt(node.offset), document.positionAt(node.offset + node.length)); } else { let overwriteStart = offset - currentWord.length; if (overwriteStart > 0 && document.getText()[overwriteStart - 1] === '"') { @@ -105,12 +112,21 @@ export class YAMLCompletion { const proposed: { [key: string]: CompletionItem } = { }; const collector: CompletionsCollector = { add: (suggestion: CompletionItem) => { - const existing = proposed[suggestion.label]; + let label = suggestion.label; + const existing = proposed[label]; if (!existing) { - proposed[suggestion.label] = suggestion; + label = label.replace(/[\n]/g, '↵'); + if (label.length > 60) { + const shortendedLabel = label.substr(0, 57).trim() + '...'; + if (!proposed[shortendedLabel]) { + label = shortendedLabel; + } + } if (overwriteRange && overwriteRange.start.line === overwriteRange.end.line) { suggestion.textEdit = TextEdit.replace(overwriteRange, suggestion.insertText); } + suggestion.label = label; + proposed[label] = suggestion; result.items.push(suggestion); } else if (!existing.documentation) { existing.documentation = suggestion.documentation; @@ -147,17 +163,17 @@ export class YAMLCompletion { let addValue = true; let currentKey = ''; - let currentProperty: Parser.PropertyASTNode = null; + let currentProperty: PropertyASTNode = null; if (node) { if (node.type === 'string') { - const stringNode = node; - if (stringNode.isKey) { - addValue = !(node.parent && (( node.parent).value)); - currentProperty = node.parent ? node.parent : null; - currentKey = document.getText().substring(node.start + 1, node.end - 1); - if (node.parent) { - node = node.parent.parent; + const parent = node.parent; + if (parent && parent.type === 'property' && parent.keyNode === node) { + addValue = !parent.valueNode; + currentProperty = parent; + currentKey = document.getText().substr(node.offset + 1, node.length - 2); + if (parent) { + node = parent.parent; } } } @@ -166,24 +182,20 @@ export class YAMLCompletion { // proposals for properties if (node && node.type === 'object') { // don't suggest properties that are already present - const properties = ( node).properties; + const properties = ( node).properties; properties.forEach(p => { if (!currentProperty || currentProperty !== p) { - proposed[p.key.value] = CompletionItem.create('__'); + proposed[p.keyNode.value] = CompletionItem.create('__'); } }); - let separatorAfter = ''; - if (addValue) { - separatorAfter = this.evaluateSeparatorAfter(document, document.offsetAt(overwriteRange.end)); - } - + const separatorAfter = ''; if (newSchema) { // property proposals with schema - this.getPropertyCompletions(document, newSchema, currentDoc, node, addValue, collector, separatorAfter); + this.getPropertyCompletions(newSchema, currentDoc, node, addValue, separatorAfter, collector, document); } - const location = node.getPath(); + const location = Parser.getNodePath(node); this.contributions.forEach(contribution => { const collectPromise = contribution.collectPropertyCompletions(document.uri, location, currentWord, addValue, false, collector); if (collectPromise) { @@ -193,7 +205,7 @@ export class YAMLCompletion { if ((!schema && currentWord.length > 0 && document.getText().charAt(offset - currentWord.length - 1) !== '"')) { collector.add({ kind: CompletionItemKind.Property, - label: this.getLabelForValue(currentWord), + label: currentWord, insertText: this.getInsertTextForProperty(currentWord, null, false, separatorAfter), insertTextFormat: InsertTextFormat.Snippet, documentation: '' @@ -202,11 +214,12 @@ export class YAMLCompletion { } // proposals for values + const types: { [type: string]: boolean } = { }; if (newSchema) { - this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector); + this.getValueCompletions(newSchema, currentDoc, node, offset, document, collector, types); } if (this.contributions.length > 0) { - this.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); + super.getContributedValueCompletions(currentDoc, node, offset, document, collector, collectionPromises); } return this.promise.all(collectionPromises).then(() => @@ -214,13 +227,8 @@ export class YAMLCompletion { }); } -private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, - doc, - node: Parser.ASTNode, - addValue: boolean, - collector: CompletionsCollector, - separatorAfter: string - ): void { + private getPropertyCompletions(schema: ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, addValue: boolean, + separatorAfter: string, collector: CompletionsCollector, document): void { const matchingSchemas = doc.getMatchingSchemas(schema.schema); matchingSchemas.forEach(s => { if (s.node === node && !s.inverted) { @@ -228,16 +236,16 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, if (schemaProperties) { Object.keys(schemaProperties).forEach((key: string) => { const propertySchema = schemaProperties[key]; - if (!propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) { + if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) { let identCompensation = ''; if (node.parent && node.parent.type === 'array') { // because there is a slash '-' to prevent the properties generated to have the correct // indent const sourceText = document.getText(); - const indexOfSlash = sourceText.lastIndexOf('-', node.start - 1); + const indexOfSlash = sourceText.lastIndexOf('-', node.offset - 1); if (indexOfSlash > 0) { // add one space to compensate the '-' - identCompensation = ' ' + sourceText.slice(indexOfSlash + 1, node.start); + identCompensation = ' ' + sourceText.slice(indexOfSlash + 1, node.offset); } } collector.add({ @@ -256,19 +264,20 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, // - 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); + this.addSchemaValueCompletions(s.schema, separatorAfter, collector, { }); } } }); } - private getValueCompletions(schema: ResolvedSchema, doc, node: Parser.ASTNode, offset: number, document: TextDocument, collector: CompletionsCollector): void { + private getValueCompletions(schema: ResolvedSchema, doc: Parser.JSONDocument, node: ASTNode, offset: number, document: TextDocument, + collector: CompletionsCollector, types: { [type: string]: boolean }): void { let offsetForSeparator = offset; let parentKey: string = null; - let valueNode: Parser.ASTNode = null; + let valueNode: ASTNode = null; if (node && (node.type === 'string' || node.type === 'number' || node.type === 'boolean')) { - offsetForSeparator = node.end; + offsetForSeparator = node.offset + node.length; valueNode = node; node = node.parent; } @@ -283,7 +292,7 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, if (nodeParent && nodeParent.type === 'object') { for (const prop in nodeParent['properties']) { const currNode = nodeParent['properties'][prop]; - if (currNode.key && currNode.key.location === node.location) { + if (currNode.keyNode && currNode.keyNode.value === node.location) { node = currNode; } } @@ -291,34 +300,33 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } if (!node) { - this.addSchemaValueCompletions(schema.schema, collector, ''); + this.addSchemaValueCompletions(schema.schema, '', collector, types); return; } - if ((node.type === 'property') && offset > ( node).colonOffset) { - const propertyNode = node; - const valueNode = propertyNode.value; - if (valueNode && offset > valueNode.end) { + if ((node.type === 'property') && offset > ( node).colonOffset) { + const valueNode = node.valueNode; + if (valueNode && offset > (valueNode.offset + valueNode.length)) { return; // we are past the value node } - parentKey = propertyNode.key.value; + parentKey = node.keyNode.value; node = node.parent; } - const separatorAfter = this.evaluateSeparatorAfter(document, offsetForSeparator); if (node && (parentKey !== null || node.type === 'array')) { + const separatorAfter = ''; const matchingSchemas = doc.getMatchingSchemas(schema.schema); matchingSchemas.forEach(s => { if (s.node === node && !s.inverted && s.schema) { if (s.schema.items) { if (Array.isArray(s.schema.items)) { - const index = this.findItemAtOffset(node, document, offset); + const index = super.findItemAtOffset(node, document, offset); if (index < s.schema.items.length) { - this.addSchemaValueCompletions(s.schema.items[index], collector, separatorAfter, true); + this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types); } - } else if (s.schema.items.type === 'object') { + } else if (typeof s.schema.items === 'object' && s.schema.items.type === 'object') { collector.add({ - kind: this.getSuggestionKind(s.schema.items.type), + kind: super.getSuggestionKind(s.schema.items.type), label: '- (array item)', documentation: `Create an item of an array${s.schema.description === undefined ? '' : '(' + s.schema.description + ')'}`, insertText: `- ${this.getInsertTextForObject(s.schema.items, separatorAfter).insertText.trimLeft()}`, @@ -326,52 +334,24 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, }); } else { - this.addSchemaValueCompletions(s.schema.items, collector, separatorAfter, true); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); } } if (s.schema.properties) { const propertySchema = s.schema.properties[parentKey]; if (propertySchema) { - this.addSchemaValueCompletions(propertySchema, collector, separatorAfter, false); + this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types); } } } }); - } - } - private getContributedValueCompletions(doc: Parser.JSONDocument, - node: Parser.ASTNode, - offset: number, - document: TextDocument, - collector: CompletionsCollector, - // tslint:disable-next-line: no-any - collectionPromises: Thenable[] - ) { - if (!node) { - this.contributions.forEach(contribution => { - const collectPromise = contribution.collectDefaultCompletions(document.uri, collector); - if (collectPromise) { - collectionPromises.push(collectPromise); - } - }); - } else { - if (node.type === 'string' || node.type === 'number' || node.type === 'boolean' || node.type === 'null') { - node = node.parent; + if (types['boolean']) { + this.addBooleanValueCompletion(true, separatorAfter, collector); + this.addBooleanValueCompletion(false, separatorAfter, collector); } - if ((node.type === 'property') && offset > ( node).colonOffset) { - const parentKey = ( node).key.value; - - const valueNode = ( node).value; - if (!valueNode || offset <= valueNode.end) { - const location = node.parent.getPath(); - this.contributions.forEach(contribution => { - const collectPromise = contribution.collectValueCompletions(document.uri, location, parentKey, collector); - if (collectPromise) { - collectionPromises.push(collectPromise); - } - }); - } + if (types['null']) { + this.addNullValueCompletion(separatorAfter, collector); } } } @@ -385,41 +365,13 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, }); } - private addSchemaValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { - const 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.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } - if (Array.isArray(schema.anyOf)) { - schema.anyOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } - if (Array.isArray(schema.oneOf)) { - schema.oneOf.forEach(s => this.addSchemaValueCompletionsCore(s, collector, types, separatorAfter, forArrayItem)); - } + private addSchemaValueCompletions(schema: JSONSchemaRef, separatorAfter: string, collector: CompletionsCollector, types: { [type: string]: boolean }): void { + super.addSchemaValueCompletions(schema, separatorAfter, collector, types); } - private addDefaultValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, arrayDepth = 0, forArrayItem = false): void { + private addDefaultValueCompletions(schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, arrayDepth = 0): void { let hasProposals = false; - if (schema.default) { + if (isDefined(schema.default)) { let type = schema.type; let value = schema.default; for (let i = arrayDepth; i > 0; i--) { @@ -428,15 +380,15 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } collector.add({ kind: this.getSuggestionKind(type), - label: forArrayItem ? `- ${this.getLabelForValue(value)}` : this.getLabelForValue(value), - insertText: forArrayItem ? `- ${this.getInsertTextForValue(value, separatorAfter)}` : this.getInsertTextForValue(value, separatorAfter), + label: value.toString(), + insertText: this.getInsertTextForValue(value, separatorAfter), insertTextFormat: InsertTextFormat.Snippet, - detail: localize('json.suggest.default', 'Default value'), + detail: localize('json.suggest.default', 'Default value') }); hasProposals = true; } - if (Array.isArray(schema['examples'])) { - schema['examples'].forEach(example => { + if (Array.isArray(schema.examples)) { + schema.examples.forEach(example => { let type = schema.type; let value = example; for (let i = arrayDepth; i > 0; i--) { @@ -445,78 +397,79 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } collector.add({ kind: this.getSuggestionKind(type), - label: this.getLabelForValue(value), + label: value, insertText: this.getInsertTextForValue(value, separatorAfter), insertTextFormat: InsertTextFormat.Snippet }); hasProposals = true; }); } - if (!hasProposals && schema.items && !Array.isArray(schema.items)) { - this.addDefaultValueCompletions(schema.items, collector, separatorAfter, arrayDepth + 1); - } - } - - private addEnumValueCompletions(schema: JSONSchema, collector: CompletionsCollector, separatorAfter: string, forArrayItem = false): void { - if (isDefined(schema['const'])) { - collector.add({ - kind: this.getSuggestionKind(schema.type), - label: this.getLabelForValue(schema['const']), - insertText: this.getInsertTextForValue(schema['const'], separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation: schema.description - }); - } - if (Array.isArray(schema.enum)) { - for (let i = 0, length = schema.enum.length; i < length; i++) { - const enm = schema.enum[i]; - let documentation = schema.description; - if (schema.enumDescriptions && i < schema.enumDescriptions.length) { - documentation = schema.enumDescriptions[i]; + if (Array.isArray(schema.defaultSnippets)) { + schema.defaultSnippets.forEach(s => { + let type = schema.type; + let value = s.body; + let label = s.label; + 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); + 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 } collector.add({ - kind: this.getSuggestionKind(schema.type), - label: forArrayItem ? `- ${this.getLabelForValue(enm)}` : this.getLabelForValue(enm), - insertText: forArrayItem ? `- ${this.getInsertTextForValue(enm, separatorAfter)}` : this.getInsertTextForValue(enm, separatorAfter), + kind: this.getSuggestionKind(type), + label, + documentation: super.fromMarkup(s.markdownDescription) || s.description, + insertText, insertTextFormat: InsertTextFormat.Snippet, - documentation + filterText }); - } + hasProposals = true; + }); } - } - - private collectTypes(schema: JSONSchema, types: { [type: string]: boolean }) { - const type = schema.type; - if (Array.isArray(type)) { - type.forEach(t => types[t] = true); - } else { - types[type] = true; + if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { + this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); } } - private addBooleanValueCompletion(value: boolean, collector: CompletionsCollector, separatorAfter: string): void { - collector.add({ - kind: this.getSuggestionKind('boolean'), - label: value ? 'true' : 'false', - insertText: this.getInsertTextForValue(value, separatorAfter), - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); + // tslint:disable-next-line:no-any + private getInsertTextForSnippetValue(value: any, separatorAfter: string): 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 stringifyObject(value, '', replacer) + separatorAfter; } - private addNullValueCompletion(collector: CompletionsCollector, separatorAfter: string): void { - collector.add({ - kind: this.getSuggestionKind('null'), - label: 'null', - insertText: 'null' + separatorAfter, - insertTextFormat: InsertTextFormat.Snippet, - documentation: '' - }); + // tslint:disable-next-line:no-any + private getLabelForSnippetValue(value: any): string { + const label = JSON.stringify(value); + return label.replace(/\$\{\d+:([^}]+)\}|\$\d+/g, '$1'); } private addCustomTagValueCompletion(collector: CompletionsCollector, separatorAfter: string, label: string): void { collector.add({ - kind: this.getSuggestionKind('string'), + kind: super.getSuggestionKind('string'), label: label, insertText: label + separatorAfter, insertTextFormat: InsertTextFormat.Snippet, @@ -524,19 +477,20 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, }); } - // tslint:disable-next-line: no-any - private getLabelForValue(value: any): string { - const label = typeof value === 'string' ? value : JSON.stringify(value); - if (label.length > 57) { - return label.substr(0, 57).trim() + '...'; - } - return label; + private addBooleanValueCompletion(value: boolean, separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('boolean'), + label: value ? 'true' : 'false', + insertText: this.getInsertTextForValue(value, separatorAfter), + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); } - // tslint:disable-next-line: no-any + // tslint:disable-next-line:no-any private getSuggestionKind(type: any): CompletionItemKind { if (Array.isArray(type)) { - // tslint:disable-next-line: no-any + // tslint:disable-next-line:no-any const array = type; type = array.length > 0 ? array[0] : null; } @@ -551,59 +505,25 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } } - private getCurrentWord(document: TextDocument, offset: number) { - let i = offset - 1; - const text = document.getText(); - while (i >= 0 && ' \t\n\r\v":{[,]}'.indexOf(text.charAt(i)) === -1) { - i--; - } - return text.substring(i + 1, offset); + private addNullValueCompletion(separatorAfter: string, collector: CompletionsCollector): void { + collector.add({ + kind: this.getSuggestionKind('null'), + label: 'null', + insertText: 'null' + separatorAfter, + insertTextFormat: InsertTextFormat.Snippet, + documentation: '' + }); } - private findItemAtOffset(node: Parser.ASTNode, document: TextDocument, offset: number) { - const scanner = Json.createScanner(document.getText(), true); - const children = node.getChildNodes(); - for (let i = children.length - 1; i >= 0; i--) { - const child = children[i]; - if (offset > child.end) { - scanner.setPosition(child.end); - const token = scanner.scan(); - if (token === Json.SyntaxKind.CommaToken && offset >= scanner.getTokenOffset() + scanner.getTokenLength()) { - return i + 1; - } - return i; - } else if (offset >= child.start) { - return i; - } - } - return 0; + // tslint:disable-next-line: no-any + private getInsertTextForValue(value: any, separatorAfter: string): string { + return this.getInsertTextForPlainText(value + separatorAfter); } - // private isInComment(document: TextDocument, start: number, offset: number) { - // let scanner = Json.createScanner(document.getText(), false); - // scanner.setPosition(start); - // let token = scanner.scan(); - // while (token !== Json.SyntaxKind.EOF && (scanner.getTokenOffset() + scanner.getTokenLength() < offset)) { - // token = scanner.scan(); - // } - // return (token === Json.SyntaxKind.LineCommentTrivia || token === Json.SyntaxKind.BlockCommentTrivia) && scanner.getTokenOffset() <= offset; - // } - private getInsertTextForPlainText(text: string): string { return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } } - // tslint:disable-next-line: no-any - private getInsertTextForValue(value: any, separatorAfter: string): string { - const text = value; - if (text === '{}') { - return '{\n\t$1\n}' + separatorAfter; - } else if (text === '[]') { - return '[\n\t$1\n]' + separatorAfter; - } - return this.getInsertTextForPlainText(text + separatorAfter); - } - private getInsertTextForObject(schema: JSONSchema, separatorAfter: string, indent = '\t', insertIndex = 1) { let insertText = ''; if (!schema.properties) { @@ -612,7 +532,7 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } Object.keys(schema.properties).forEach((key: string) => { - const propertySchema = schema.properties[key]; + const propertySchema = schema.properties[key] as JSONSchema; let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; if (!type) { if (propertySchema.properties) { @@ -663,7 +583,8 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, return { insertText, insertIndex }; } - private getInsertTextForArray(schema: JSONSchema, separatorAfter: string, indent = '\t', insertIndex = 1) { + // tslint:disable-next-line:no-any + private getInsertTextForArray(schema: any, separatorAfter: string, indent = '\t', insertIndex = 1) { let insertText = ''; if (!schema) { insertText = `\$${insertIndex++}`; @@ -701,21 +622,52 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, ident: string = '\t'): string { const propertyText = this.getInsertTextForValue(key, ''); - // if (!addValue) { - // return propertyText; - // } const resultText = propertyText + ':'; let value; + let nValueProposals = 0; if (propertySchema) { - if (propertySchema.default !== undefined) { - value = ` \${1:${propertySchema.default}}`; - } else if (propertySchema.properties) { + if (Array.isArray(propertySchema.defaultSnippets)) { + if (propertySchema.defaultSnippets.length === 1) { + const body = propertySchema.defaultSnippets[0].body; + if (isDefined(body)) { + value = this.getInsertTextForSnippetValue(body, ''); + } + } + nValueProposals += propertySchema.defaultSnippets.length; + } + if (propertySchema.enum) { + if (!value && propertySchema.enum.length === 1) { + value = this.getInsertTextForGuessedValue(propertySchema.enum[0], ''); + } + nValueProposals += propertySchema.enum.length; + } + if (isDefined(propertySchema.default)) { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.default, ''); + } + nValueProposals++; + } + if (Array.isArray(propertySchema.examples) && propertySchema.examples.length) { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.examples[0], ''); + } + nValueProposals += propertySchema.examples.length; + } + if (propertySchema.properties) { return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, ident).insertText}`; } else if (propertySchema.items) { return `${resultText}\n\t- ${this.getInsertTextForArray(propertySchema.items, separatorAfter, ident).insertText}`; - } else { - const type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; + } + if (nValueProposals === 0) { + let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; + if (!type) { + if (propertySchema.properties) { + type = 'object'; + } else if (propertySchema.items) { + type = 'array'; + } + } switch (type) { case 'boolean': value = ' $1'; @@ -741,26 +693,34 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, } } } - if (!value) { + if (!value || nValueProposals > 1) { value = '$1'; } return resultText + value + separatorAfter; } - private evaluateSeparatorAfter(document: TextDocument, offset: number) { - // let scanner = Json.createScanner(document.getText(), true); - // scanner.setPosition(offset); - // let token = scanner.scan(); - // switch (token) { - // case Json.SyntaxKind.CommaToken: - // case Json.SyntaxKind.CloseBraceToken: - // case Json.SyntaxKind.CloseBracketToken: - // case Json.SyntaxKind.EOF: - // return ''; - // default: - // return ''; - // } - return ''; + // tslint:disable-next-line:no-any + private getInsertTextForGuessedValue(value: any, separatorAfter: string): string { + switch (typeof value) { + case 'object': + if (value === null) { + return '${1:null}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + case 'string': + let snippetValue = JSON.stringify(value); + snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes + snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } + return '${1:' + snippetValue + '}' + separatorAfter; + case 'number': + case 'boolean': + return '${1:' + value + '}' + separatorAfter; + } + return this.getInsertTextForValue(value, separatorAfter); + } + + private getLabelForValue(value: string) { + return value; } /** @@ -827,9 +787,7 @@ private getPropertyCompletions(document: TextDocument, schema: ResolvedSchema, // Called by onCompletion private setKubernetesParserOption(jsonDocuments: Parser.JSONDocument[], option: boolean) { for (const jsonDoc in jsonDocuments) { - jsonDocuments[jsonDoc].configureSettings({ - isKubernetes: option - }); + jsonDocuments[jsonDoc].isKubernetes = option; } } } diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index dc74a000..be4a4524 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -7,7 +7,7 @@ import { PromiseConstructor, Thenable, LanguageService } from 'vscode-json-languageservice'; import { Hover, TextDocument, Position } from 'vscode-languageserver-types'; -import { matchOffsetToDocument2 } from '../utils/arrUtils'; +import { matchOffsetToDocument } from '../utils/arrUtils'; import { LanguageSettings } from '../yamlLanguageService'; import { parse as parseYAML } from '../parser/yamlParser07'; import { YAMLSchemaService } from './yamlSchemaService'; @@ -38,7 +38,7 @@ export class YAMLHover { } const doc = parseYAML(document.getText()); const offset = document.offsetAt(position); - const currentDoc = matchOffsetToDocument2(offset, doc); + const currentDoc = matchOffsetToDocument(offset, doc); if (currentDoc === null) { return this.promise.resolve(void 0); } diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index 8be07e5a..bec88f51 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -8,7 +8,7 @@ import { Diagnostic, TextDocument } from 'vscode-languageserver-types'; import { PromiseConstructor, LanguageSettings } from '../yamlLanguageService'; import { parse as parseYAML, YAMLDocument } from '../parser/yamlParser07'; -import { SingleYAMLDocument } from '../parser/yamlParser04'; +import { SingleYAMLDocument } from '../parser/yamlParser07'; import { YAMLSchemaService } from './yamlSchemaService'; import { JSONValidation } from 'vscode-json-languageservice/lib/umd/services/jsonValidation'; diff --git a/src/languageservice/utils/arrUtils.ts b/src/languageservice/utils/arrUtils.ts index 988c206c..1557157b 100644 --- a/src/languageservice/utils/arrUtils.ts +++ b/src/languageservice/utils/arrUtils.ts @@ -2,7 +2,7 @@ * Copyright (c) Red Hat, Inc. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SingleYAMLDocument } from '../parser/yamlParser04'; +import { SingleYAMLDocument } from '../parser/yamlParser07'; export function removeDuplicates(arr, prop) { const new_arr = []; @@ -63,19 +63,6 @@ export function removeDuplicatesObj(objArray){ export function matchOffsetToDocument(offset: number, jsonDocuments) { - for (const jsonDoc in jsonDocuments.documents) { - const currJsonDoc: SingleYAMLDocument = jsonDocuments.documents[jsonDoc]; - if (currJsonDoc.root && currJsonDoc.root.end >= offset && currJsonDoc.root.start <= offset) { - return currJsonDoc; - } - } - - // TODO: Fix this so that it returns the correct document - return jsonDocuments.documents[0]; -} - -export function matchOffsetToDocument2(offset: number, jsonDocuments) { - for (const jsonDoc of jsonDocuments.documents) { if (jsonDoc.root && jsonDoc.root.offset <= offset && (jsonDoc.root.length + jsonDoc.root.offset) >= offset) { return jsonDoc; diff --git a/test/autoCompletion2.test.ts b/test/autoCompletion2.test.ts index 937d92bd..ccbffe89 100644 --- a/test/autoCompletion2.test.ts +++ b/test/autoCompletion2.test.ts @@ -6,7 +6,7 @@ import { TextDocument } from 'vscode-languageserver'; import { getLanguageService } from '../src/languageservice/yamlLanguageService'; import { YAMLSchemaService } from '../src/languageservice/services/yamlSchemaService'; import { schemaRequestService, workspaceContext } from './utils/testHelper'; -import { parse as parseYAML } from '../src/languageservice/parser/yamlParser04'; +import { parse as parseYAML } from '../src/languageservice/parser/yamlParser07'; import { getLineOffsets } from '../src/languageservice/utils/arrUtils'; import assert = require('assert');