From 777d15d564f5a8eac12112941bb506129b5409c9 Mon Sep 17 00:00:00 2001 From: rickcowan Date: Tue, 26 Jul 2022 20:27:03 -0500 Subject: [PATCH 01/14] fix: default snippet in array --- .../services/yamlCompletion.ts | 34 +++++++++++++------ test/autoCompletionFix.test.ts | 33 ++++++++++++++++++ 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index de70bf6f4..7afb56f73 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -867,7 +867,7 @@ export class YamlCompletion { insertTextFormat: InsertTextFormat.Snippet, }); - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, true); } else if (typeof s.schema.items === 'object' && s.schema.items.anyOf) { s.schema.items.anyOf .filter((i) => typeof i === 'object') @@ -889,7 +889,7 @@ export class YamlCompletion { insertTextFormat: InsertTextFormat.Snippet, }); }); - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, true); } else { this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); } @@ -1255,21 +1255,21 @@ export class YamlCompletion { ): void { if (typeof schema === 'object') { this.addEnumValueCompletions(schema, separatorAfter, collector, isArray); - this.addDefaultValueCompletions(schema, separatorAfter, collector); + this.addDefaultValueCompletions(schema, separatorAfter, collector, 0, isArray); this.collectTypes(schema, types); if (Array.isArray(schema.allOf)) { schema.allOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); }); } if (Array.isArray(schema.anyOf)) { schema.anyOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); }); } if (Array.isArray(schema.oneOf)) { schema.oneOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); }); } } @@ -1293,7 +1293,8 @@ export class YamlCompletion { schema: JSONSchema, separatorAfter: string, collector: CompletionsCollector, - arrayDepth = 0 + arrayDepth = 0, + isArray?: boolean ): void { let hasProposals = false; if (isDefined(schema.default)) { @@ -1335,11 +1336,21 @@ export class YamlCompletion { hasProposals = true; }); } - this.collectDefaultSnippets(schema, separatorAfter, collector, { + + let stringifySettings = { newLineFirst: true, indentFirstObject: true, shouldIndentWithTab: true, - }); + }; + + if (isArray) { + stringifySettings = { + newLineFirst: false, + indentFirstObject: false, + shouldIndentWithTab: false, + }; + } + this.collectDefaultSnippets(schema, separatorAfter, collector, stringifySettings, 0, isArray); if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); } @@ -1395,7 +1406,8 @@ export class YamlCompletion { separatorAfter: string, collector: CompletionsCollector, settings: StringifySettings, - arrayDepth = 0 + arrayDepth = 0, + isArray?: boolean ): void { if (Array.isArray(schema.defaultSnippets)) { for (const s of schema.defaultSnippets) { @@ -1406,7 +1418,7 @@ export class YamlCompletion { let filterText: string; if (isDefined(value)) { const type = s.type || schema.type; - if (arrayDepth === 0 && type === 'array') { + if ((arrayDepth === 0 && type === 'array') || isArray) { // We know that a - isn't present yet so we need to add one const fixedObj = {}; Object.keys(value).forEach((val, index) => { diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 0526fd1cc..ec53bd2d1 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -402,6 +402,39 @@ objB: }) ); }); + it('Autocomplete with snippet without hypen (-) inside an array', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + defaultSnippets: [ + { + label: 'My array item', + body: { item1: '$1' }, + }, + ], + required: ['thing1'], + properties: { + thing1: { + type: 'object', + required: ['item1'], + properties: { + item1: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }); + const content = 'array1:\n - thing1:\n item1: $1\n | |'; + const completion = await parseCaret(content); + + expect(completion.items[1].insertText).to.be.equal('- item1: '); + }); describe('array indent on different index position', () => { const schema = { type: 'object', From 9726605caed7edc949e4cffc3a80f2def0409487 Mon Sep 17 00:00:00 2001 From: rickcowan Date: Wed, 27 Jul 2022 16:11:16 -0500 Subject: [PATCH 02/14] Cleaned up collectDefaultSnippets call --- .../services/yamlCompletion.ts | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 7afb56f73..0dd7c3221 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1337,20 +1337,18 @@ export class YamlCompletion { }); } - let stringifySettings = { - newLineFirst: true, - indentFirstObject: true, - shouldIndentWithTab: true, - }; - - if (isArray) { - stringifySettings = { - newLineFirst: false, - indentFirstObject: false, - shouldIndentWithTab: false, - }; - } - this.collectDefaultSnippets(schema, separatorAfter, collector, stringifySettings, 0, isArray); + this.collectDefaultSnippets( + schema, + separatorAfter, + collector, + { + newLineFirst: !isArray, + indentFirstObject: !isArray, + shouldIndentWithTab: !isArray, + }, + 0, + isArray + ); if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); } @@ -1407,7 +1405,7 @@ export class YamlCompletion { collector: CompletionsCollector, settings: StringifySettings, arrayDepth = 0, - isArray?: boolean + isArray = false ): void { if (Array.isArray(schema.defaultSnippets)) { for (const s of schema.defaultSnippets) { From c6c9a2d50fb2cd72810498fc66d742c54b645136 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Fri, 11 Nov 2022 11:03:48 +0100 Subject: [PATCH 03/14] Merge branch 'main' into fix/main-default-snippet-in-array --- .github/workflows/CI.yaml | 4 +- .github/workflows/release.yaml | 4 +- CHANGELOG.md | 10 + README.md | 2 + package.json | 16 +- .../handlers/settingsHandlers.ts | 6 + src/languageservice/parser/jsonParser07.ts | 73 +++- .../services/validation/yaml-style.ts | 47 +++ .../services/yamlCodeActions.ts | 29 ++ .../services/yamlCompletion.ts | 218 ++++++---- src/languageservice/services/yamlHover.ts | 47 ++- .../services/yamlValidation.ts | 23 +- .../utils/flow-style-rewriter.ts | 55 +++ src/languageservice/utils/ranges.ts | 42 -- src/languageservice/utils/schemaUtils.ts | 10 +- src/languageservice/yamlLanguageService.ts | 9 + src/server.ts | 5 +- src/yamlSettings.ts | 8 + test/autoCompletion.test.ts | 74 +++- test/autoCompletionFix.test.ts | 264 +++++++++++- test/defaultSnippets.test.ts | 5 +- test/fixtures/testArrayCompletionSchema.json | 153 ++++--- test/flow-style-rewriter.test.ts | 94 +++++ test/hover.test.ts | 91 +++++ test/schemaValidation.test.ts | 160 +++++++- test/settingsHandlers.test.ts | 33 ++ test/utils/serviceSetup.ts | 11 + test/utils/verifyError.ts | 7 +- test/yamlCodeActions.test.ts | 29 ++ test/yamlCodeLens.test.ts | 15 + test/yamlValidation.test.ts | 83 +++- tsconfig.json | 7 +- yarn.lock | 386 ++++++++---------- 33 files changed, 1532 insertions(+), 488 deletions(-) create mode 100644 src/languageservice/services/validation/yaml-style.ts create mode 100644 src/languageservice/utils/flow-style-rewriter.ts delete mode 100644 src/languageservice/utils/ranges.ts create mode 100644 test/flow-style-rewriter.test.ts diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 6bd6f43fd..71adafd49 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -26,10 +26,10 @@ jobs: - uses: actions/checkout@v2 # Set up Node - - name: Use Node 12 + - name: Use Node 16 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 16 registry-url: "https://registry.npmjs.org" # Run install dependencies diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0a9b307fc..3365dfd15 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,10 +23,10 @@ jobs: - uses: actions/checkout@v2 # Set up Node - - name: Use Node 12 + - name: Use Node 16 uses: actions/setup-node@v1 with: - node-version: 12 + node-version: 16 registry-url: 'https://registry.npmjs.org' # Run install dependencies diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e30e8d71..62288e09b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +### 1.10.0 +- Fix: TypeError: i.startsWith is not a function [#747](https://github.com/redhat-developer/yaml-language-server/issues/747) +- Fix: fix: autocomplete indent on object within an array [#751](https://github.com/redhat-developer/yaml-language-server/pull/751) +- Add: Yaml style linting to forbid flow style [#753](https://github.com/redhat-developer/yaml-language-server/pull/753) +- Fix: enum validation [#803](https://github.com/redhat-developer/vscode-yaml/issues/803) +- Fix: autocompletion problem when value is null inside anyOf object [#684](https://github.com/redhat-developer/yaml-language-server/issues/684) +- Fix: indentation with extra spaces after cursor. [#764](https://github.com/redhat-developer/yaml-language-server/pull/764) + +Thanks to Rickcowan + ### 1.9.0 - Add: Publish pre-release extension on nightly CI build [#682](https://github.com/redhat-developer/vscode-yaml/issues/682) - Add: Add title to extension configuration [#793](https://github.com/redhat-developer/vscode-yaml/pull/793) diff --git a/README.md b/README.md index abdadcc44..f06a86619 100755 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ The following settings are supported: - `[yaml].editor.formatOnType`: Enable/disable on type indent and auto formatting array - `yaml.disableDefaultProperties`: Disable adding not required properties with default values into completion text - `yaml.suggest.parentSkeletonSelectedFirst`: If true, the user must select some parent skeleton first before autocompletion starts to suggest the rest of the properties.\nWhen yaml object is not empty, autocompletion ignores this setting and returns all properties and skeletons. +- `yaml.style.flowMapping` : Forbids flow style mappings if set to `forbid` +- `yaml.style.flowSequence` : Forbids flow style sequences if set to `forbid` ##### Adding custom tags diff --git a/package.json b/package.json index 359274537..93bfb7310 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "yaml-language-server", "description": "YAML language server", - "version": "1.10.0", + "version": "1.11.0", "author": "Gorkem Ercan (Red Hat)", "license": "MIT", "contributors": [ @@ -46,18 +46,18 @@ "devDependencies": { "@types/chai": "^4.2.12", "@types/mocha": "8.2.2", - "@types/node": "^12.11.7", + "@types/node": "16.x", "@types/prettier": "2.0.2", "@types/sinon": "^9.0.5", "@types/sinon-chai": "^3.2.5", - "@typescript-eslint/eslint-plugin": "^5.30.0", - "@typescript-eslint/parser": "^5.30.0", + "@typescript-eslint/eslint-plugin": "^5.38.0", + "@typescript-eslint/parser": "^5.38.0", "chai": "^4.2.0", "coveralls": "3.1.1", - "eslint": "^7.2.0", - "eslint-config-prettier": "^6.11.0", + "eslint": "^8.24.0", + "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", - "eslint-plugin-prettier": "^3.1.4", + "eslint-plugin-prettier": "^4.2.1", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "mocha": "9.2.2", @@ -68,7 +68,7 @@ "sinon-chai": "^3.5.0", "source-map-support": "^0.5.19", "ts-node": "^10.0.0", - "typescript": "^4.7.0" + "typescript": "^4.8.3" }, "scripts": { "clean": "rimraf out/server && rimraf lib", diff --git a/src/languageserver/handlers/settingsHandlers.ts b/src/languageserver/handlers/settingsHandlers.ts index 5be99371c..b88774ec5 100644 --- a/src/languageserver/handlers/settingsHandlers.ts +++ b/src/languageserver/handlers/settingsHandlers.ts @@ -118,6 +118,10 @@ export class SettingsHandler { if (settings.yaml.suggest) { this.yamlSettings.suggest.parentSkeletonSelectedFirst = settings.yaml.suggest.parentSkeletonSelectedFirst; } + this.yamlSettings.style = { + flowMapping: settings.yaml.style?.flowMapping ?? 'allow', + flowSequence: settings.yaml.style?.flowSequence ?? 'allow', + }; } this.yamlSettings.schemaConfigurationSettings = []; @@ -250,6 +254,8 @@ export class SettingsHandler { disableAdditionalProperties: this.yamlSettings.disableAdditionalProperties, disableDefaultProperties: this.yamlSettings.disableDefaultProperties, parentSkeletonSelectedFirst: this.yamlSettings.suggest.parentSkeletonSelectedFirst, + flowMapping: this.yamlSettings.style?.flowMapping, + flowSequence: this.yamlSettings.style?.flowSequence, yamlVersion: this.yamlSettings.yamlVersion, }; diff --git a/src/languageservice/parser/jsonParser07.ts b/src/languageservice/parser/jsonParser07.ts index 6d53d2123..a254f3f7f 100644 --- a/src/languageservice/parser/jsonParser07.ts +++ b/src/languageservice/parser/jsonParser07.ts @@ -80,6 +80,7 @@ export interface IProblem { problemType?: ProblemType; problemArgs?: string[]; schemaUri?: string[]; + data?: Record; } export abstract class ASTNodeImpl { @@ -145,7 +146,7 @@ export abstract class ASTNodeImpl { } export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode { - public type: 'null' = 'null'; + public type: 'null' = 'null' as const; public value = null; constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) { super(parent, internalNode, offset, length); @@ -153,7 +154,7 @@ export class NullASTNodeImpl extends ASTNodeImpl implements NullASTNode { } export class BooleanASTNodeImpl extends ASTNodeImpl implements BooleanASTNode { - public type: 'boolean' = 'boolean'; + public type: 'boolean' = 'boolean' as const; public value: boolean; constructor(parent: ASTNode, internalNode: Node, boolValue: boolean, offset: number, length?: number) { @@ -163,7 +164,7 @@ export class BooleanASTNodeImpl extends ASTNodeImpl implements BooleanASTNode { } export class ArrayASTNodeImpl extends ASTNodeImpl implements ArrayASTNode { - public type: 'array' = 'array'; + public type: 'array' = 'array' as const; public items: ASTNode[]; constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) { @@ -177,7 +178,7 @@ export class ArrayASTNodeImpl extends ASTNodeImpl implements ArrayASTNode { } export class NumberASTNodeImpl extends ASTNodeImpl implements NumberASTNode { - public type: 'number' = 'number'; + public type: 'number' = 'number' as const; public isInteger: boolean; public value: number; @@ -189,7 +190,7 @@ export class NumberASTNodeImpl extends ASTNodeImpl implements NumberASTNode { } export class StringASTNodeImpl extends ASTNodeImpl implements StringASTNode { - public type: 'string' = 'string'; + public type: 'string' = 'string' as const; public value: string; constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) { @@ -199,7 +200,7 @@ export class StringASTNodeImpl extends ASTNodeImpl implements StringASTNode { } export class PropertyASTNodeImpl extends ASTNodeImpl implements PropertyASTNode { - public type: 'property' = 'property'; + public type: 'property' = 'property' as const; public keyNode: StringASTNode; public valueNode: ASTNode; public colonOffset: number; @@ -215,7 +216,7 @@ export class PropertyASTNodeImpl extends ASTNodeImpl implements PropertyASTNode } export class ObjectASTNodeImpl extends ASTNodeImpl implements ObjectASTNode { - public type: 'object' = 'object'; + public type: 'object' = 'object' as const; public properties: PropertyASTNode[]; constructor(parent: ASTNode, internalNode: Node, offset: number, length?: number) { @@ -573,19 +574,39 @@ export class JSONDocument { p.code ? p.code : ErrorCode.Undefined, p.source ); - diagnostic.data = { schemaUri: p.schemaUri }; + diagnostic.data = { schemaUri: p.schemaUri, ...p.data }; return diagnostic; }); } return null; } - public getMatchingSchemas(schema: JSONSchema, focusOffset = -1, exclude: ASTNode = null): IApplicableSchema[] { + /** + * This method returns the list of applicable schemas + * + * currently used @param didCallFromAutoComplete flag to differentiate the method call, when it is from auto complete + * then user still types something and skip the validation for timebeing untill completed. + * On https://github.com/redhat-developer/yaml-language-server/pull/719 the auto completes need to populate the list of enum string which matches to the enum + * and on https://github.com/redhat-developer/vscode-yaml/issues/803 the validation should throw the error based on the enum string. + * + * @param schema schema + * @param focusOffset offsetValue + * @param exclude excluded Node + * @param didCallFromAutoComplete true if method called from AutoComplete + * @returns array of applicable schemas + */ + public getMatchingSchemas( + schema: JSONSchema, + focusOffset = -1, + exclude: ASTNode = null, + didCallFromAutoComplete?: boolean + ): IApplicableSchema[] { const matchingSchemas = new SchemaCollector(focusOffset, exclude); if (this.root && schema) { validate(this.root, schema, schema, new ValidationResult(this.isKubernetes), matchingSchemas, { isKubernetes: this.isKubernetes, disableAdditionalProperties: this.disableAdditionalProperties, + callFromAutoComplete: didCallFromAutoComplete, }); } return matchingSchemas.schemas; @@ -594,6 +615,7 @@ export class JSONDocument { interface Options { isKubernetes: boolean; disableAdditionalProperties: boolean; + callFromAutoComplete?: boolean; } function validate( node: ASTNode, @@ -604,7 +626,7 @@ function validate( options: Options // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { - const { isKubernetes } = options; + const { isKubernetes, callFromAutoComplete } = options; if (!node) { return; } @@ -699,6 +721,7 @@ function validate( const testAlternatives = (alternatives: JSONSchemaRef[], maxOneMatch: boolean): number => { const matches = []; + const subMatches = []; const noPropertyMatches = []; // remember the best match that is used for error messages let bestMatch: { @@ -707,15 +730,19 @@ function validate( matchingSchemas: ISchemaCollector; } = null; for (const subSchemaRef of alternatives) { - const subSchema = asSchema(subSchemaRef); + const subSchema = { ...asSchema(subSchemaRef) }; const subValidationResult = new ValidationResult(isKubernetes); const subMatchingSchemas = matchingSchemas.newSub(); validate(node, subSchema, schema, subValidationResult, subMatchingSchemas, options); - if (!subValidationResult.hasProblems()) { + if (!subValidationResult.hasProblems() || callFromAutoComplete) { matches.push(subSchema); + subMatches.push(subSchema); if (subValidationResult.propertiesMatches === 0) { noPropertyMatches.push(subSchema); } + if (subSchema.format) { + subMatches.pop(); + } } if (!bestMatch) { bestMatch = { @@ -730,11 +757,11 @@ function validate( } } - if (matches.length > 1 && noPropertyMatches.length === 0 && maxOneMatch) { + if (subMatches.length > 1 && (subMatches.length > 1 || noPropertyMatches.length === 0) && maxOneMatch) { validationResult.problems.push({ location: { offset: node.offset, length: 1 }, severity: DiagnosticSeverity.Warning, - message: localize('oneOfWarning', 'Minimum one schema should validate.'), + message: localize('oneOfWarning', 'Matches multiple schemas when only one must validate.'), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), }); @@ -797,7 +824,7 @@ function validate( const val = getNodeValue(node); let enumValueMatch = false; for (const e of schema.enum) { - if (equals(val, e) || (typeof val === 'string' && val && e.startsWith(val))) { + if (equals(val, e) || (callFromAutoComplete && isString(val) && isString(e) && val && e.startsWith(val))) { enumValueMatch = true; break; } @@ -1295,6 +1322,8 @@ function validate( (schema.type === 'object' && schema.additionalProperties === undefined && options.disableAdditionalProperties === true) ) { if (unprocessedProperties.length > 0) { + const possibleProperties = schema.properties && Object.keys(schema.properties).filter((prop) => !seenKeys[prop]); + for (const propertyName of unprocessedProperties) { const child = seenKeys[propertyName]; if (child) { @@ -1307,7 +1336,7 @@ function validate( } else { propertyNode = child; } - validationResult.problems.push({ + const problem: IProblem = { location: { offset: propertyNode.keyNode.offset, length: propertyNode.keyNode.length, @@ -1317,7 +1346,11 @@ function validate( schema.errorMessage || localize('DisallowedExtraPropWarning', 'Property {0} is not allowed.', propertyName), source: getSchemaSource(schema, originalSchema), schemaUri: getSchemaUri(schema, originalSchema), - }); + }; + if (possibleProperties?.length) { + problem.data = { properties: possibleProperties }; + } + validationResult.problems.push(problem); } } } @@ -1435,7 +1468,11 @@ function validate( validationResult: ValidationResult; matchingSchemas: ISchemaCollector; } { - if (!maxOneMatch && !subValidationResult.hasProblems() && !bestMatch.validationResult.hasProblems()) { + if ( + !maxOneMatch && + !subValidationResult.hasProblems() && + (!bestMatch.validationResult.hasProblems() || callFromAutoComplete) + ) { // no errors, both are equally good matches bestMatch.matchingSchemas.merge(subMatchingSchemas); bestMatch.validationResult.propertiesMatches += subValidationResult.propertiesMatches; diff --git a/src/languageservice/services/validation/yaml-style.ts b/src/languageservice/services/validation/yaml-style.ts new file mode 100644 index 000000000..bb43bc813 --- /dev/null +++ b/src/languageservice/services/validation/yaml-style.ts @@ -0,0 +1,47 @@ +import { TextDocument } from 'vscode-languageserver-textdocument'; +import { Diagnostic, DiagnosticSeverity, Range } from 'vscode-languageserver-types'; +import { isMap, isSeq, visit } from 'yaml'; +import { FlowCollection } from 'yaml/dist/parse/cst'; +import { SingleYAMLDocument } from '../../parser/yaml-documents'; +import { LanguageSettings } from '../../yamlLanguageService'; +import { AdditionalValidator } from './types'; + +export class YAMLStyleValidator implements AdditionalValidator { + private forbidSequence: boolean; + private forbidMapping: boolean; + + constructor(settings: LanguageSettings) { + this.forbidMapping = settings.flowMapping === 'forbid'; + this.forbidSequence = settings.flowSequence === 'forbid'; + } + validate(document: TextDocument, yamlDoc: SingleYAMLDocument): Diagnostic[] { + const result = []; + visit(yamlDoc.internalDocument, (key, node) => { + if (this.forbidMapping && isMap(node) && node.srcToken?.type === 'flow-collection') { + result.push( + Diagnostic.create( + this.getRangeOf(document, node.srcToken), + 'Flow style mapping is forbidden', + DiagnosticSeverity.Error, + 'flowMap' + ) + ); + } + if (this.forbidSequence && isSeq(node) && node.srcToken?.type === 'flow-collection') { + result.push( + Diagnostic.create( + this.getRangeOf(document, node.srcToken), + 'Flow style sequence is forbidden', + DiagnosticSeverity.Error, + 'flowSeq' + ) + ); + } + }); + return result; + } + + private getRangeOf(document: TextDocument, node: FlowCollection): Range { + return Range.create(document.positionAt(node.start.offset), document.positionAt(node.end.pop().offset)); + } +} diff --git a/src/languageservice/services/yamlCodeActions.ts b/src/languageservice/services/yamlCodeActions.ts index 57360c74b..b2b07ab83 100644 --- a/src/languageservice/services/yamlCodeActions.ts +++ b/src/languageservice/services/yamlCodeActions.ts @@ -21,6 +21,10 @@ import { TextBuffer } from '../utils/textBuffer'; import { LanguageSettings } from '../yamlLanguageService'; import { YAML_SOURCE } from '../parser/jsonParser07'; import { getFirstNonWhitespaceCharacterAfterOffset } from '../utils/strings'; +import { matchOffsetToDocument } from '../utils/arrUtils'; +import { isMap, isSeq } from 'yaml'; +import { yamlDocumentsCache } from '../parser/yaml-documents'; +import { FlowStyleRewriter } from '../utils/flow-style-rewriter'; interface YamlDiagnosticData { schemaUri: string[]; @@ -45,6 +49,7 @@ export class YamlCodeActions { result.push(...this.getJumpToSchemaActions(params.context.diagnostics)); result.push(...this.getTabToSpaceConverting(params.context.diagnostics, document)); result.push(...this.getUnusedAnchorsDelete(params.context.diagnostics, document)); + result.push(...this.getConvertToBlockStyleActions(params.context.diagnostics, document)); return result; } @@ -207,6 +212,30 @@ export class YamlCodeActions { } return results; } + + private getConvertToBlockStyleActions(diagnostics: Diagnostic[], document: TextDocument): CodeAction[] { + const results: CodeAction[] = []; + for (const diagnostic of diagnostics) { + if (diagnostic.code === 'flowMap' || diagnostic.code === 'flowSeq') { + const yamlDocuments = yamlDocumentsCache.getYamlDocument(document); + const startOffset = document.offsetAt(diagnostic.range.start); + const yamlDoc = matchOffsetToDocument(startOffset, yamlDocuments); + const node = yamlDoc.getNodeFromOffset(startOffset); + if (isMap(node.internalNode) || isSeq(node.internalNode)) { + const blockTypeDescription = isMap(node.internalNode) ? 'map' : 'sequence'; + const rewriter = new FlowStyleRewriter(this.indentation); + results.push( + CodeAction.create( + `Convert to block style ${blockTypeDescription}`, + createWorkspaceEdit(document.uri, [TextEdit.replace(diagnostic.range, rewriter.write(node))]), + CodeActionKind.QuickFix + ) + ); + } + } + } + return results; + } } function createWorkspaceEdit(uri: string, edits: TextEdit[]): WorkspaceEdit { diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 0dd7c3221..57d59acab 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -35,7 +35,7 @@ import { setKubernetesParserOption } from '../parser/isKubernetes'; import { asSchema } from '../parser/jsonParser07'; import { indexOf, isInComment, isMapContainsEmptyPair } from '../utils/astUtils'; import { isModeline } from './modelineUtil'; -import { getSchemaTypeName } from '../utils/schemaUtils'; +import { getSchemaTypeName, isAnyOfAllOfOneOfType, isPrimitiveType } from '../utils/schemaUtils'; import { YamlNode } from '../jsonASTTypes'; const localize = nls.loadMessageBundle(); @@ -54,10 +54,11 @@ interface CompletionItem extends CompletionItemBase { parent?: ParentCompletionItemOptions; } interface CompletionsCollector { - add(suggestion: CompletionItem): void; + add(suggestion: CompletionItem, oneOfSchema?: boolean): void; error(message: string): void; log(message: string): void; getNumberOfProposals(): number; + result: CompletionList; } interface InsertText { @@ -94,7 +95,7 @@ export class YamlCompletion { this.parentSkeletonSelectedFirst = languageSettings.parentSkeletonSelectedFirst; } - async doComplete(document: TextDocument, position: Position, isKubernetes = false): Promise { + async doComplete(document: TextDocument, position: Position, isKubernetes = false, doComplete = true): Promise { const result = CompletionList.create([], false); if (!this.completionEnabled) { return result; @@ -138,7 +139,8 @@ export class YamlCompletion { if (areOnlySpacesAfterPosition) { overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); const isOnlyWhitespace = lineContent.trim().length === 0; - if (node && isScalar(node) && !isOnlyWhitespace) { + const isOnlyDash = lineContent.match(/^\s*(-)\s*$/); + if (node && isScalar(node) && !isOnlyWhitespace && !isOnlyDash) { // line contains part of a key with trailing spaces, adjust the overwrite range to include only the text const matches = lineContent.match(/^([\s-]*)[^:]+[ \t]+\n?$/); if (matches?.length) { @@ -174,7 +176,7 @@ export class YamlCompletion { const proposed: { [key: string]: CompletionItem } = {}; const existingProposeItem = '__'; const collector: CompletionsCollector = { - add: (completionItem: CompletionItem) => { + add: (completionItem: CompletionItem, oneOfSchema: boolean) => { const addSuggestionForParent = function (completionItem: CompletionItem): void { const existsInYaml = proposed[completionItem.label]?.label === existingProposeItem; //don't put to parent suggestion if already in yaml @@ -201,6 +203,7 @@ export class YamlCompletion { sortText: '_' + schemaType, // this parent completion goes first, kind: parentCompletionKind, }; + parentCompletion.label = parentCompletion.label || completionItem.label; parentCompletion.parent.insertTexts = [completionItem.insertText]; result.items.push(parentCompletion); } else { @@ -255,7 +258,7 @@ export class YamlCompletion { result.items.push(completionItem); } else if (isInsertTextDifferent) { // try to merge simple insert values - const mergedText = this.mergeSimpleInsertTexts(label, existing.insertText, completionItem.insertText); + const mergedText = this.mergeSimpleInsertTexts(label, existing.insertText, completionItem.insertText, oneOfSchema); if (mergedText) { this.updateCompletionText(existing, mergedText); } else { @@ -277,6 +280,7 @@ export class YamlCompletion { getNumberOfProposals: () => { return result.items.length; }, + result, }; if (this.customTags.length > 0) { @@ -489,7 +493,17 @@ export class YamlCompletion { } } - this.addPropertyCompletions(schema, currentDoc, node, originalNode, '', collector, textBuffer, overwriteRange); + this.addPropertyCompletions( + schema, + currentDoc, + node, + originalNode, + '', + collector, + textBuffer, + overwriteRange, + doComplete + ); if (!schema && currentWord.length > 0 && text.charAt(offset - currentWord.length - 1) !== '"') { collector.add({ @@ -503,13 +517,31 @@ export class YamlCompletion { // proposals for values const types: { [type: string]: boolean } = {}; - this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types); + this.getValueCompletions(schema, currentDoc, node, offset, document, collector, types, doComplete); } catch (err) { this.telemetry.sendError('yaml.completion.error', { error: convertErrorToTelemetryMsg(err) }); } this.finalizeParentCompletion(result); + const uniqueItems = result.items.filter( + (arr, index, self) => + index === + self.findIndex( + (item) => + item.label === arr.label && + item.label === arr.label && + item.insertText === arr.insertText && + item.insertText === arr.insertText && + item.kind === arr.kind && + item.kind === arr.kind + ) + ); + + if (uniqueItems?.length > 0) { + result.items = uniqueItems; + } + return result; } @@ -520,11 +552,22 @@ export class YamlCompletion { } } - mergeSimpleInsertTexts(label: string, existingText: string, addingText: string): string | undefined { + mergeSimpleInsertTexts(label: string, existingText: string, addingText: string, oneOfSchema: boolean): string | undefined { const containsNewLineAfterColon = (value: string): boolean => { return value.includes('\n'); }; + const startWithNewLine = (value: string): boolean => { + return value.startsWith('\n'); + }; + const isNullObject = (value: string): boolean => { + const index = value.indexOf('\n'); + return index > 0 && value.substring(index, value.length).trim().length === 0; + }; if (containsNewLineAfterColon(existingText) || containsNewLineAfterColon(addingText)) { + //if the exisiting object null one then replace with the non-null object + if (oneOfSchema && isNullObject(existingText) && !isNullObject(addingText) && !startWithNewLine(addingText)) { + return addingText; + } return undefined; } const existingValues = this.getValuesFromInsertText(existingText); @@ -621,9 +664,10 @@ export class YamlCompletion { separatorAfter: string, collector: CompletionsCollector, textBuffer: TextBuffer, - overwriteRange: Range + overwriteRange: Range, + doComplete: boolean ): void { - const matchingSchemas = doc.getMatchingSchemas(schema.schema); + const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete); const existingKey = textBuffer.getText(overwriteRange); const lineContent = textBuffer.getLineContent(overwriteRange.start.line); const hasOnlyWhitespace = lineContent.trim().length === 0; @@ -631,6 +675,15 @@ export class YamlCompletion { const isInArray = lineContent.trimLeft().indexOf('-') === 0; const nodeParent = doc.getParent(node); const matchOriginal = matchingSchemas.find((it) => it.node.internalNode === originalNode && it.schema.properties); + const oneOfSchema = matchingSchemas.filter((schema) => schema.schema.oneOf).map((oneOfSchema) => oneOfSchema.schema.oneOf)[0]; + let didOneOfSchemaMatches = false; + if (oneOfSchema?.length < matchingSchemas.length) { + oneOfSchema?.forEach((property: JSONSchema, index: number) => { + if (!matchingSchemas[index]?.schema.oneOf && matchingSchemas[index]?.schema.properties === property.properties) { + didOneOfSchemaMatches = true; + } + }); + } for (const schema of matchingSchemas) { if ( ((schema.node.internalNode === node && !matchOriginal) || (schema.node.internalNode === originalNode && !hasColon)) && @@ -687,24 +740,9 @@ export class YamlCompletion { pair ) { if (Array.isArray(propertySchema.items)) { - this.addSchemaValueCompletions(propertySchema.items[0], separatorAfter, collector, {}); + this.addSchemaValueCompletions(propertySchema.items[0], separatorAfter, collector, {}, 'property'); } else if (typeof propertySchema.items === 'object' && propertySchema.items.type === 'object') { - const insertText = `- ${this.getInsertTextForObject( - propertySchema.items, - separatorAfter, - ' ' - ).insertText.trimLeft()}`; - const documentation = this.getDocumentationWithMarkdownText( - `Create an item of an array${propertySchema.description ? ' (' + propertySchema.description + ')' : ''}`, - insertText - ); - collector.add({ - kind: this.getSuggestionKind(propertySchema.items.type), - label: '- (array item)', - documentation, - insertText, - insertTextFormat: InsertTextFormat.Snippet, - }); + this.addArrayItemValueCompletion(propertySchema.items, separatorAfter, collector); } } @@ -722,13 +760,16 @@ export class YamlCompletion { (isMap(originalNode) && originalNode.items.length === 0); const existsParentCompletion = schema.schema.required?.length > 0; if (!this.parentSkeletonSelectedFirst || !isNodeNull || !existsParentCompletion) { - collector.add({ - kind: CompletionItemKind.Property, - label: key, - insertText, - insertTextFormat: InsertTextFormat.Snippet, - documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '', - }); + collector.add( + { + kind: CompletionItemKind.Property, + label: key, + insertText, + insertTextFormat: InsertTextFormat.Snippet, + documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '', + }, + didOneOfSchemaMatches + ); } // if the prop is required add it also to parent suggestion if (schema.schema.required?.includes(key)) { @@ -758,8 +799,15 @@ export class YamlCompletion { // test: // - item1 // it will treated as a property key since `:` has been appended - if (nodeParent && isSeq(nodeParent) && schema.schema.type !== 'object') { - this.addSchemaValueCompletions(schema.schema, separatorAfter, collector, {}, Array.isArray(nodeParent.items)); + if (nodeParent && isSeq(nodeParent) && isPrimitiveType(schema.schema)) { + this.addSchemaValueCompletions( + schema.schema, + separatorAfter, + collector, + {}, + 'property', + Array.isArray(nodeParent.items) + ); } if (schema.schema.propertyNames && schema.schema.additionalProperties && schema.schema.type === 'object') { @@ -814,7 +862,8 @@ export class YamlCompletion { offset: number, document: TextDocument, collector: CompletionsCollector, - types: { [type: string]: boolean } + types: { [type: string]: boolean }, + doComplete: boolean ): void { let parentKey: string = null; @@ -823,7 +872,7 @@ export class YamlCompletion { } if (!node) { - this.addSchemaValueCompletions(schema.schema, '', collector, types); + this.addSchemaValueCompletions(schema.schema, '', collector, types, 'value'); return; } @@ -838,7 +887,7 @@ export class YamlCompletion { if (node && (parentKey !== null || isSeq(node))) { const separatorAfter = ''; - const matchingSchemas = doc.getMatchingSchemas(schema.schema); + const matchingSchemas = doc.getMatchingSchemas(schema.schema, -1, null, doComplete); for (const s of matchingSchemas) { if (s.node.internalNode === node && !s.inverted && s.schema) { if (s.schema.items) { @@ -851,57 +900,25 @@ export class YamlCompletion { if (Array.isArray(s.schema.items)) { const index = this.findItemAtOffset(node, document, offset); if (index < s.schema.items.length) { - this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types); + this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, 'value'); } - } else if (typeof s.schema.items === 'object' && s.schema.items.type === 'object') { - const insertText = `- ${this.getInsertTextForObject(s.schema.items, separatorAfter, ' ').insertText.trimLeft()}`; - const documentation = this.getDocumentationWithMarkdownText( - `Create an item of an array${s.schema.description ? ' (' + s.schema.description + ')' : ''}`, - insertText - ); - collector.add({ - kind: this.getSuggestionKind(s.schema.items.type), - label: '- (array item)', - documentation, - insertText, - insertTextFormat: InsertTextFormat.Snippet, - }); - - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, true); - } else if (typeof s.schema.items === 'object' && s.schema.items.anyOf) { - s.schema.items.anyOf - .filter((i) => typeof i === 'object') - .forEach((i: JSONSchema, index) => { - const schemaType = getSchemaTypeName(i); - const insertText = `- ${this.getInsertTextForObject(i, separatorAfter).insertText.trimLeft()}`; - //append insertText to documentation - const schemaTypeTitle = schemaType ? ' type `' + schemaType + '`' : ''; - const schemaDescription = s.schema.description ? ' (' + s.schema.description + ')' : ''; - const documentation = this.getDocumentationWithMarkdownText( - `Create an item of an array${schemaTypeTitle}${schemaDescription}`, - insertText - ); - collector.add({ - kind: this.getSuggestionKind(i.type), - label: '- (array item) ' + (schemaType || index + 1), - documentation: documentation, - insertText: insertText, - insertTextFormat: InsertTextFormat.Snippet, - }); - }); - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, true); + } else if ( + typeof s.schema.items === 'object' && + (s.schema.items.type === 'object' || isAnyOfAllOfOneOfType(s.schema.items)) + ) { + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } else { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value'); } } } if (s.schema.properties) { const propertySchema = s.schema.properties[parentKey]; if (propertySchema) { - this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types); + this.addSchemaValueCompletions(propertySchema, separatorAfter, collector, types, 'value'); } } else if (s.schema.additionalProperties) { - this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types); + this.addSchemaValueCompletions(s.schema.additionalProperties, separatorAfter, collector, types, 'value'); } } } @@ -916,6 +933,30 @@ export class YamlCompletion { } } + private addArrayItemValueCompletion( + schema: JSONSchema, + separatorAfter: string, + collector: CompletionsCollector, + index?: number + ): void { + const schemaType = getSchemaTypeName(schema); + const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimLeft()}`; + //append insertText to documentation + const schemaTypeTitle = schemaType ? ' type `' + schemaType + '`' : ''; + const schemaDescription = schema.description ? ' (' + schema.description + ')' : ''; + const documentation = this.getDocumentationWithMarkdownText( + `Create an item of an array${schemaTypeTitle}${schemaDescription}`, + insertText + ); + collector.add({ + kind: this.getSuggestionKind(schema.type), + label: '- (array item) ' + (schemaType || index), + documentation: documentation, + insertText: insertText, + insertTextFormat: InsertTextFormat.Snippet, + }); + } + private getInsertTextForProperty( key: string, propertySchema: JSONSchema, @@ -1082,7 +1123,7 @@ export class YamlCompletion { if (arrayInsertLines.length > 1) { for (let index = 1; index < arrayInsertLines.length; index++) { const element = arrayInsertLines[index]; - arrayInsertLines[index] = `${indent}${this.indentation} ${element.trimLeft()}`; + arrayInsertLines[index] = ` ${element}`; } arrayTemplate = arrayInsertLines.join('\n'); } @@ -1251,25 +1292,32 @@ export class YamlCompletion { separatorAfter: string, collector: CompletionsCollector, types: unknown, + completionType: 'property' | 'value', isArray?: boolean ): void { if (typeof schema === 'object') { this.addEnumValueCompletions(schema, separatorAfter, collector, isArray); this.addDefaultValueCompletions(schema, separatorAfter, collector, 0, isArray); this.collectTypes(schema, types); + + if (isArray && completionType === 'value' && !isAnyOfAllOfOneOfType(schema)) { + // add array only for final types (no anyOf, allOf, oneOf) + this.addArrayItemValueCompletion(schema, separatorAfter, collector); + } + if (Array.isArray(schema.allOf)) { schema.allOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); }); } if (Array.isArray(schema.anyOf)) { schema.anyOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); }); } if (Array.isArray(schema.oneOf)) { schema.oneOf.forEach((s) => { - return this.addSchemaValueCompletions(s, separatorAfter, collector, types, isArray); + return this.addSchemaValueCompletions(s, separatorAfter, collector, types, completionType, isArray); }); } } diff --git a/src/languageservice/services/yamlHover.ts b/src/languageservice/services/yamlHover.ts index 7991fed3f..ad0d14d55 100644 --- a/src/languageservice/services/yamlHover.ts +++ b/src/languageservice/services/yamlHover.ts @@ -13,12 +13,13 @@ import { setKubernetesParserOption } from '../parser/isKubernetes'; import { TextDocument } from 'vscode-languageserver-textdocument'; import { yamlDocumentsCache } from '../parser/yaml-documents'; import { SingleYAMLDocument } from '../parser/yamlParser07'; -import { getNodeValue } from '../parser/jsonParser07'; +import { getNodeValue, IApplicableSchema } from '../parser/jsonParser07'; import { JSONSchema } from '../jsonSchema'; import { URI } from 'vscode-uri'; import * as path from 'path'; import { Telemetry } from '../../languageserver/telemetry'; import { convertErrorToTelemetryMsg } from '../utils/objects'; +import { ASTNode } from 'vscode-json-languageservice'; export class YAMLHover { private shouldHover: boolean; @@ -96,6 +97,10 @@ export class YAMLHover { return result; }; + const removePipe = (value: string): string => { + return value.replace(/\|\|\s*$/, ''); + }; + return this.schemaService.getSchemaForResource(document.uri, doc).then((schema) => { if (schema && node && !schema.errors.length) { const matchingSchemas = doc.getMatchingSchemas(schema.schema, node.offset); @@ -124,6 +129,21 @@ export class YAMLHover { } } } + if (s.schema.anyOf && isAllSchemasMatched(node, matchingSchemas, s.schema)) { + //if append title and description of all matched schemas on hover + title = ''; + markdownDescription = ''; + s.schema.anyOf.forEach((childSchema: JSONSchema, index: number) => { + title += childSchema.title || s.schema.closestTitle || ''; + markdownDescription += childSchema.markdownDescription || toMarkdown(childSchema.description) || ''; + if (index !== s.schema.anyOf.length - 1) { + title += ' || '; + markdownDescription += ' || '; + } + }); + title = removePipe(title); + markdownDescription = removePipe(markdownDescription); + } if (s.schema.examples) { s.schema.examples.forEach((example) => { markdownExamples.push(JSON.stringify(example)); @@ -198,3 +218,28 @@ function toMarkdownCodeBlock(content: string): string { } return content; } + +/** + * check all the schemas which is inside anyOf presented or not in matching schema. + * @param node node + * @param matchingSchemas all matching schema + * @param schema scheam which is having anyOf + * @returns true if all the schemas which inside anyOf presents in matching schema + */ +function isAllSchemasMatched(node: ASTNode, matchingSchemas: IApplicableSchema[], schema: JSONSchema): boolean { + let count = 0; + for (const matchSchema of matchingSchemas) { + if (node === matchSchema.node && matchSchema.schema !== schema) { + schema.anyOf.forEach((childSchema: JSONSchema) => { + if ( + matchSchema.schema.title === childSchema.title && + matchSchema.schema.description === childSchema.description && + matchSchema.schema.properties === childSchema.properties + ) { + count++; + } + }); + } + } + return count === schema.anyOf.length; +} diff --git a/src/languageservice/services/yamlValidation.ts b/src/languageservice/services/yamlValidation.ts index 4f9588e0c..912d7842c 100644 --- a/src/languageservice/services/yamlValidation.ts +++ b/src/languageservice/services/yamlValidation.ts @@ -19,6 +19,7 @@ import { convertErrorToTelemetryMsg } from '../utils/objects'; import { Telemetry } from '../../languageserver/telemetry'; import { AdditionalValidator } from './validation/types'; import { UnusedAnchorsValidator } from './validation/unused-anchors'; +import { YAMLStyleValidator } from './validation/yaml-style'; /** * Convert a YAMLDocDiagnostic to a language server Diagnostic @@ -43,23 +44,28 @@ export class YAMLValidation { private jsonValidation; private disableAdditionalProperties: boolean; private yamlVersion: YamlVersion; - private additionalValidation: AdditionalValidation; + private validators: AdditionalValidator[] = []; private MATCHES_MULTIPLE = 'Matches multiple schemas when only one must validate.'; constructor(schemaService: YAMLSchemaService, private readonly telemetry: Telemetry) { this.validationEnabled = true; this.jsonValidation = new JSONValidation(schemaService, Promise); - this.additionalValidation = new AdditionalValidation(); } public configure(settings: LanguageSettings): void { + this.validators = []; if (settings) { this.validationEnabled = settings.validate; this.customTags = settings.customTags; this.disableAdditionalProperties = settings.disableAdditionalProperties; this.yamlVersion = settings.yamlVersion; + // Add style validator if flow style is set to forbid only. + if (settings.flowMapping === 'forbid' || settings.flowSequence === 'forbid') { + this.validators.push(new YAMLStyleValidator(settings)); + } } + this.validators.push(new UnusedAnchorsValidator()); } public async doValidation(textDocument: TextDocument, isKubernetes = false): Promise { @@ -93,11 +99,11 @@ export class YAMLValidation { } validationResult.push(...validation); - validationResult.push(...this.additionalValidation.validate(textDocument, currentYAMLDoc)); + validationResult.push(...this.runAdditionalValidators(textDocument, currentYAMLDoc)); index++; } } catch (err) { - this.telemetry.sendError('yaml.validation.error', convertErrorToTelemetryMsg(err)); + this.telemetry.sendError('yaml.validation.error', { error: convertErrorToTelemetryMsg(err) }); } let previousErr: Diagnostic; @@ -142,14 +148,7 @@ export class YAMLValidation { return duplicateMessagesRemoved; } -} - -class AdditionalValidation { - private validators: AdditionalValidator[] = []; - constructor() { - this.validators.push(new UnusedAnchorsValidator()); - } - validate(document: TextDocument, yarnDoc: SingleYAMLDocument): Diagnostic[] { + private runAdditionalValidators(document: TextDocument, yarnDoc: SingleYAMLDocument): Diagnostic[] { const result = []; for (const validator of this.validators) { diff --git a/src/languageservice/utils/flow-style-rewriter.ts b/src/languageservice/utils/flow-style-rewriter.ts new file mode 100644 index 000000000..6e977203d --- /dev/null +++ b/src/languageservice/utils/flow-style-rewriter.ts @@ -0,0 +1,55 @@ +import { CST, visit } from 'yaml'; +import { SourceToken } from 'yaml/dist/parse/cst'; +import { ASTNode } from '../jsonASTTypes'; + +export class FlowStyleRewriter { + constructor(private readonly indentation: string) {} + + public write(node: ASTNode): string | null { + if (node.internalNode.srcToken['type'] !== 'flow-collection') { + return null; + } + const collection: CST.FlowCollection = node.internalNode.srcToken as CST.FlowCollection; + const blockType = collection.start.type === 'flow-map-start' ? 'block-map' : 'block-seq'; + const parentType = node.parent.type; + + const blockStyle = { + type: blockType, + offset: collection.offset, + indent: collection.indent, + items: [], + }; + + for (const item of collection.items) { + CST.visit(item, ({ key, sep, value }) => { + if (blockType === 'block-map') { + const start = [{ type: 'space', indent: 0, offset: key.offset, source: this.indentation } as SourceToken]; + if (parentType === 'property') { + // add a new line if part of a map + start.unshift({ type: 'newline', indent: 0, offset: key.offset, source: '\n' } as SourceToken); + } + blockStyle.items.push({ + start: start, + key: key, + sep: sep, + value: value, + }); + } else if (blockType === 'block-seq') { + blockStyle.items.push({ + start: [ + { type: 'newline', indent: 0, offset: value.offset, source: '\n' } as SourceToken, + { type: 'space', indent: 0, offset: value.offset, source: this.indentation } as SourceToken, + { type: 'seq-item-ind', indent: 0, offset: value.offset, source: '-' } as SourceToken, + { type: 'space', indent: 0, offset: value.offset, source: ' ' } as SourceToken, + ], + value: value, + }); + } + if (value.type === 'flow-collection') { + return visit.SKIP; + } + }); + } + return CST.stringify(blockStyle as CST.Token); + } +} diff --git a/src/languageservice/utils/ranges.ts b/src/languageservice/utils/ranges.ts deleted file mode 100644 index 8e0291107..000000000 --- a/src/languageservice/utils/ranges.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Red Hat, Inc. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Range } from 'vscode-languageserver-types'; - -/** - * Check if rangeA and rangeB is intersect - * @param rangeA - * @param rangeB - */ -export function isIntersect(rangeA: Range, rangeB: Range): boolean { - if ( - rangeA.start.line >= rangeB.start.line && - rangeA.start.character >= rangeB.start.character && - rangeA.start.line <= rangeB.end.line && - rangeA.start.character <= rangeB.end.character - ) { - return true; - } - - if ( - rangeA.end.line >= rangeB.start.line && - rangeA.end.character >= rangeB.start.character && - rangeA.end.line <= rangeB.end.line && - rangeA.end.character <= rangeB.end.character - ) { - return true; - } - - if ( - rangeA.start.line >= rangeB.start.line && - rangeA.start.character >= rangeB.start.character && - rangeA.end.line <= rangeB.end.line && - rangeA.end.character <= rangeB.end.character - ) { - return true; - } - - return false; -} diff --git a/src/languageservice/utils/schemaUtils.ts b/src/languageservice/utils/schemaUtils.ts index 0c689f2dd..6926db01a 100644 --- a/src/languageservice/utils/schemaUtils.ts +++ b/src/languageservice/utils/schemaUtils.ts @@ -50,8 +50,16 @@ export function getSchemaTitle(schema: JSONSchema, url: string): string { if (Object.getOwnPropertyDescriptor(schema, 'name')) { return Object.getOwnPropertyDescriptor(schema, 'name').value + ` (${baseName})`; } else if (schema.title) { - return schema.title + ` (${baseName})`; + return schema.description ? schema.title + ' - ' + schema.description + ` (${baseName})` : schema.title + ` (${baseName})`; } return baseName; } + +export function isPrimitiveType(schema: JSONSchema): boolean { + return schema.type !== 'object' && !isAnyOfAllOfOneOfType(schema); +} + +export function isAnyOfAllOfOneOfType(schema: JSONSchema): boolean { + return !!(schema.anyOf || schema.allOf || schema.oneOf); +} diff --git a/src/languageservice/yamlLanguageService.ts b/src/languageservice/yamlLanguageService.ts index 780668523..9c47e015b 100644 --- a/src/languageservice/yamlLanguageService.ts +++ b/src/languageservice/yamlLanguageService.ts @@ -107,6 +107,15 @@ export interface LanguageSettings { * Default yaml lang version */ yamlVersion?: YamlVersion; + + /** + * Control the use of flow mappings. Default is allow. + */ + flowMapping?: 'allow' | 'forbid'; + /** + * Control the use of flow sequences. Default is allow. + */ + flowSequence?: 'allow' | 'forbid'; } export interface WorkspaceContextService { diff --git a/src/server.ts b/src/server.ts index 35c7830c8..c500639c5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -47,8 +47,9 @@ console.error = (arg) => { const yamlSettings = new SettingsState(); const fileSystem = { - readFile: (fsPath: string, encoding?: string) => { - return fs.readFile(fsPath, encoding).then((b) => b.toString()); + readFile: async (fsPath: string, encoding?: string) => { + const b = await fs.readFile(fsPath, encoding as BufferEncoding); + return b.toString(); }, }; diff --git a/src/yamlSettings.ts b/src/yamlSettings.ts index 445194d0a..ee241517d 100644 --- a/src/yamlSettings.ts +++ b/src/yamlSettings.ts @@ -25,6 +25,10 @@ export interface Settings { suggest: { parentSkeletonSelectedFirst: boolean; }; + style: { + flowMapping: 'allow' | 'forbid'; + flowSequence: 'allow' | 'forbid'; + }; maxItemsComputed: number; yamlVersion: YamlVersion; }; @@ -78,6 +82,10 @@ export class SettingsState { suggest = { parentSkeletonSelectedFirst: false, }; + style: { + flowMapping: 'allow' | 'forbid'; + flowSequence: 'allow' | 'forbid'; + }; maxItemsComputed = 5000; // File validation helpers diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 5d9cf9b24..05fb18012 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1050,8 +1050,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 1); assert.deepEqual( result.items[0], - createExpectedCompletion('- (array item)', '- ', 2, 2, 2, 2, 9, 2, { - documentation: { kind: 'markdown', value: 'Create an item of an array\n ```\n- \n```' }, + createExpectedCompletion('- (array item) object', '- ', 2, 2, 2, 2, 9, 2, { + documentation: { kind: 'markdown', value: 'Create an item of an array type `object`\n ```\n- \n```' }, }) ); }) @@ -1085,8 +1085,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 1); assert.deepEqual( result.items[0], - createExpectedCompletion('- (array item)', '- ', 1, 0, 1, 0, 9, 2, { - documentation: { kind: 'markdown', value: 'Create an item of an array\n ```\n- \n```' }, + createExpectedCompletion('- (array item) object', '- ', 1, 0, 1, 0, 9, 2, { + documentation: { kind: 'markdown', value: 'Create an item of an array type `object`\n ```\n- \n```' }, }) ); }) @@ -1306,8 +1306,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 1); assert.deepEqual( result.items[0], - createExpectedCompletion('- (array item)', '- name: ${1:test}', 3, 4, 3, 5, 9, 2, { - documentation: { kind: 'markdown', value: 'Create an item of an array\n ```\n- name: test\n```' }, + createExpectedCompletion('- (array item) object', '- name: ${1:test}', 3, 4, 3, 5, 9, 2, { + documentation: { kind: 'markdown', value: 'Create an item of an array type `object`\n ```\n- name: test\n```' }, }) ); }) @@ -2529,7 +2529,7 @@ describe('Auto Completion Tests', () => { completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].label, '- (array item)'); + assert.equal(result.items[0].label, '- (array item) obj1'); }) .then(done, done); }); @@ -2586,7 +2586,7 @@ describe('Auto Completion Tests', () => { completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].label, '- (array item)'); + assert.equal(result.items[0].label, '- (array item) obj1'); }) .then(done, done); }); @@ -2598,8 +2598,19 @@ describe('Auto Completion Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 2); - assert.equal(result.items[0].label, '- (array item) obj1'); + expect(result.items.map((i) => i.label)).deep.eq(['- (array item) obj1', '- (array item) obj2']); + }) + .then(done, done); + }); + + it('Array nested anyOf without "-" should return all array items', (done) => { + const schema = require(path.join(__dirname, './fixtures/testArrayCompletionSchema.json')); + languageService.addSchema(SCHEMA_ID, schema); + const content = 'test_array_nested_anyOf:\n - obj1:\n name:1\n '; + const completion = parseSetup(content, content.length); + completion + .then(function (result) { + expect(result.items.map((i) => i.label)).deep.eq(['- (array item) obj1', '- (array item) obj2', '- (array item) obj3']); }) .then(done, done); }); @@ -2704,6 +2715,46 @@ describe('Auto Completion Tests', () => { required: ['type', 'options'], type: 'object', }; + it('Should suggest all possible option in oneOf when content empty', async () => { + const schema = { + type: 'object', + oneOf: [ + { + additionalProperties: false, + properties: { + A: { + type: 'string', + }, + }, + required: ['A'], + }, + { + additionalProperties: false, + properties: { + B: { + type: 'string', + }, + }, + required: ['B'], + }, + ], + }; + languageService.addSchema(SCHEMA_ID, schema); + const content = ''; + const result = await parseSetup(content, content.length); + + expect(result.items.length).equal(4); + expect(result.items[0]).to.deep.equal( + createExpectedCompletion('A', 'A: ', 0, 0, 0, 0, 10, 2, { + documentation: '', + }) + ); + expect(result.items[2]).to.deep.equal( + createExpectedCompletion('B', 'B: ', 0, 0, 0, 0, 10, 2, { + documentation: '', + }) + ); + }); it('Should suggest complete object skeleton', async () => { const schema = { definitions: { obj1, obj2 }, @@ -2814,7 +2865,7 @@ describe('Auto Completion Tests', () => { const content = ''; const result = await parseSetup(content, content.length); - expect(result.items.length).equal(4); + expect(result.items.length).equal(3); expect(result.items[1]).to.deep.equal( createExpectedCompletion('Object1', 'type: typeObj1\noptions:\n label: ', 0, 0, 0, 0, 7, 2, { documentation: { @@ -2824,7 +2875,6 @@ describe('Auto Completion Tests', () => { sortText: '_Object1', }) ); - expect(result.items[1]).to.deep.equal(result.items[3]); }); it('Should suggest rest of the parent object', async () => { const schema = { diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index ec53bd2d1..d05163505 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -217,6 +217,83 @@ objB: ); }); + it('Should suggest valid matches from oneOf', async () => { + languageService.addSchema(SCHEMA_ID, { + oneOf: [ + { + type: 'object', + properties: { + spec: { + type: 'object', + }, + }, + }, + { + properties: { + spec: { + type: 'object', + required: ['bar'], + properties: { + bar: { + type: 'string', + }, + }, + }, + }, + }, + ], + }); + const content = '|s|'; // len: 1, pos: 1 + const completion = await parseCaret(content); + expect(completion.items.length).equal(1); + expect(completion.items[0]).to.be.deep.equal( + createExpectedCompletion('spec', 'spec:\n bar: ', 0, 0, 0, 1, 10, 2, { + documentation: '', + }) + ); + }); + + it('Should suggest all the matches from allOf', async () => { + languageService.addSchema(SCHEMA_ID, { + allOf: [ + { + type: 'object', + properties: { + spec: { + type: 'object', + }, + }, + }, + { + properties: { + spec: { + type: 'object', + required: ['bar'], + properties: { + bar: { + type: 'string', + }, + }, + }, + }, + }, + ], + }); + const content = '|s|'; // len: 1, pos: 1 + const completion = await parseCaret(content); + expect(completion.items.length).equal(2); + expect(completion.items[0]).to.be.deep.equal( + createExpectedCompletion('spec', 'spec:\n ', 0, 0, 0, 1, 10, 2, { + documentation: '', + }) + ); + expect(completion.items[1]).to.be.deep.equal( + createExpectedCompletion('spec', 'spec:\n bar: ', 0, 0, 0, 1, 10, 2, { + documentation: '', + }) + ); + }); + it('Autocomplete with a new line inside the object', async () => { languageService.addSchema(SCHEMA_ID, { type: 'object', @@ -275,10 +352,10 @@ objB: const completion = await parseCaret(content); expect(completion.items.length).equal(1); expect(completion.items[0]).to.be.deep.equal( - createExpectedCompletion('- (array item)', '- ', 1, 2, 1, 2, 9, 2, { + createExpectedCompletion('- (array item) object', '- ', 1, 2, 1, 2, 9, 2, { documentation: { kind: 'markdown', - value: 'Create an item of an array\n ```\n- \n```', + value: 'Create an item of an array type `object`\n ```\n- \n```', }, }) ); @@ -402,6 +479,54 @@ objB: }) ); }); + it('Autocomplete indent on array object when parent is array of an array', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + required: ['thing1'], + properties: { + thing1: { + type: 'object', + required: ['array2'], + properties: { + array2: { + type: 'array', + items: { + type: 'object', + required: ['thing2', 'type'], + properties: { + type: { + type: 'string', + }, + thing2: { + type: 'object', + required: ['item1', 'item2'], + properties: { + item1: { type: 'string' }, + item2: { type: 'string' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + const content = 'array1:\n - '; + const completion = await parseSetup(content, 1, 4); + + expect(completion.items[0].insertText).to.be.equal( + 'thing1:\n array2:\n - type: $1\n thing2:\n item1: $2\n item2: $3' + ); + }); it('Autocomplete with snippet without hypen (-) inside an array', async () => { languageService.addSchema(SCHEMA_ID, { type: 'object', @@ -433,7 +558,7 @@ objB: const content = 'array1:\n - thing1:\n item1: $1\n | |'; const completion = await parseCaret(content); - expect(completion.items[1].insertText).to.be.equal('- item1: '); + expect(completion.items[0].insertText).to.be.equal('- item1: '); }); describe('array indent on different index position', () => { const schema = { @@ -597,7 +722,35 @@ objB: const completion = await parseSetup(content, 0, 9); // before line brake expect(completion.items.length).equal(0); }); + + it('autoCompletion when value is null inside anyOf object', async () => { + const schema: JSONSchema = { + anyOf: [ + { + properties: { + prop: { + const: 'const value', + }, + }, + }, + { + properties: { + prop: { + type: 'null', + }, + }, + }, + ], + }; + languageService.addSchema(SCHEMA_ID, schema); + const content = ''; + const completion = await parseSetup(content, 0, 6); + expect(completion.items.length).equal(1); + expect(completion.items[0].label).to.be.equal('prop'); + expect(completion.items[0].insertText).to.be.equal('prop: ${1|const value,null|}'); + }); }); + describe('extra space after cursor', () => { it('simple const', async () => { const schema: JSONSchema = { @@ -804,6 +957,111 @@ objB: }); }); }); + it('array completion - should suggest correct indent when extra spaces after cursor', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + test: { + type: 'array', + items: { + type: 'object', + properties: { + objA: { + type: 'object', + required: ['itemA'], + properties: { + itemA: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }); + const content = 'test:\n - '; + const result = await parseSetup(content, 1, 4); + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].insertText).to.be.equal('objA:\n itemA: '); + }); + it('array of arrays completion - should suggest correct indent when extra spaces after cursor', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + required: ['array2'], + properties: { + array2: { + type: 'array', + items: { + type: 'object', + required: ['objA'], + properties: { + objA: { + type: 'object', + required: ['itemA'], + properties: { + itemA: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + const content = 'array1:\n - '; + const result = await parseSetup(content, 1, 4); + + expect(result.items.length).to.be.equal(2); + expect(result.items[0].insertText).to.be.equal('array2:\n - objA:\n itemA: '); + }); + it('object of array of arrays completion - should suggest correct indent when extra spaces after cursor', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + properties: { + array2: { + type: 'array', + items: { + type: 'object', + properties: { + objA: { + type: 'object', + required: ['itemA'], + properties: { + itemA: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }); + const content = 'array1:\n - array2:\n - '; + const result = await parseSetup(content, 2, 8); + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].insertText).to.be.equal('objA:\n itemA: '); + }); }); //'extra space after cursor' it('should suggest from additionalProperties', async () => { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 9ecf276d1..b032913d2 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -308,8 +308,11 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 4); + assert.equal(result.items.length, 2); assert.equal(result.items[0].insertText, 'item1: $1\n item2: $2'); + assert.equal(result.items[1].insertText, '\n item1: $1\n item2: $2'); + // test failing here. result is '- item1: $1\n item2: $2', but it's not correct + // two results are not correct probably }) .then(done, done); }); diff --git a/test/fixtures/testArrayCompletionSchema.json b/test/fixtures/testArrayCompletionSchema.json index 0accf9ee4..9916eb896 100644 --- a/test/fixtures/testArrayCompletionSchema.json +++ b/test/fixtures/testArrayCompletionSchema.json @@ -1,85 +1,110 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "obj1": { - "properties": { - "obj1": { - "type": "object" - } - }, - "required": [ - "obj1" - ], - "type": "object" + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "obj1": { + "properties": { + "obj1": { + "type": "object" + } }, - "obj2": { - "properties": { - "obj2": { - "type": "object" - } - }, - "required": [ - "obj2" - ], - "type": "object" - } + "required": ["obj1"], + "type": "object" }, - "properties": { - "test_simpleArrayObject": { - "items": { - "$ref": "#/definitions/obj1" - }, - "type": "array" + "obj2": { + "properties": { + "obj2": { + "type": "object" + } }, - "test_array_anyOf_2objects": { - "items": { - "anyOf": [ - { - "$ref": "#/definitions/obj1" - }, - { - "$ref": "#/definitions/obj2" - } - ] - }, - "type": "array" + "required": ["obj2"], + "type": "object" + }, + "obj3": { + "properties": { + "obj3": { + "type": "object" + } }, - "test_array_anyOf_strAndObj": { - "items": { - "anyOf": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/obj1" - } - ] - }, - "type": "array" + "required": ["obj3"], + "type": "object" + } + }, + "properties": { + "test_simpleArrayObject": { + "items": { + "$ref": "#/definitions/obj1" }, - "test_anyOfObjectAndNull": { + "type": "array" + }, + "test_array_anyOf_2objects": { + "items": { "anyOf": [ { "$ref": "#/definitions/obj1" }, { - "type": "null" + "$ref": "#/definitions/obj2" } ] }, - "test_anyOfArrAndNull": { + "type": "array" + }, + "test_array_anyOf_strAndObj": { + "items": { "anyOf": [ { - "type": "array", - "items": { - "type": "string" - } + "type": "string" }, { - "type": "null" + "$ref": "#/definitions/obj1" } ] - } + }, + "type": "array" }, - "type": "object" - } + "test_anyOfObjectAndNull": { + "anyOf": [ + { + "$ref": "#/definitions/obj1" + }, + { + "type": "null" + } + ] + }, + "test_anyOfArrAndNull": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "test_array_nested_anyOf": { + "items": { + "anyOf": [ + { + "anyOf": [ + { + "$ref": "#/definitions/obj1" + }, + { + "$ref": "#/definitions/obj2" + } + ] + }, + { + "$ref": "#/definitions/obj3" + } + ] + }, + "type": "array" + } + }, + "type": "object" +} diff --git a/test/flow-style-rewriter.test.ts b/test/flow-style-rewriter.test.ts new file mode 100644 index 000000000..7cc773755 --- /dev/null +++ b/test/flow-style-rewriter.test.ts @@ -0,0 +1,94 @@ +import { expect } from 'chai'; +import { YamlDocuments } from '../src/languageservice/parser/yaml-documents'; +import { FlowStyleRewriter } from '../src/languageservice/utils/flow-style-rewriter'; +import { setupTextDocument } from './utils/testHelper'; + +describe('Flow style rewriter', () => { + let writer: FlowStyleRewriter; + let documents: YamlDocuments; + const indentation = ' '; + beforeEach(() => { + documents = new YamlDocuments(); + writer = new FlowStyleRewriter(indentation); + }); + + it('should return null if node is not flow style', () => { + const doc = setupTextDocument('foo: bar'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(1); + const result = writer.write(node); + expect(result).to.be.null; + }); + + it('should rewrite flow style map to block', () => { + const doc = setupTextDocument('datacenter: { location: canada, cab: 15}'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(13); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(`\n${indentation}location: canada\n${indentation}cab: 15`); + }); + + it('should rewrite flow style map and preserve space ', () => { + const doc = setupTextDocument('datacenter: { location: canada, cab: 15}'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(13); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(`\n${indentation}location: canada\n${indentation}cab: 15`); + }); + + it('should rewrite flow style map with null ', () => { + const doc = setupTextDocument('datacenter: { "explicit": "entry",\n "implicit": "entry",\n null: null }'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(13); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals( + `\n${indentation}"explicit": "entry"\n${indentation}"implicit": "entry"\n${indentation}null: null ` + ); + }); + + it('should rewrite flow style map with explicit entry', () => { + const doc = setupTextDocument('datacenter: { "foo bar": "baz" }'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(13); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(`\n${indentation}"foo bar": "baz" `); + }); + + it('should rewrite flow style sequence', () => { + const doc = setupTextDocument('animals: [dog , cat , mouse] '); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(9); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(`\n${indentation}- dog \n${indentation}- cat \n${indentation}- mouse`); + }); + + it('should rewrite flow style for mixed sequence and map', () => { + const doc = setupTextDocument('animals: [ { "foo": "bar" } ]'); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(9); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(`\n${indentation}- { "foo": "bar" } `); + }); + it('should rewrite flow style when parent is sequence', () => { + const doc = setupTextDocument(`items:\n${indentation}- { location: some }`); + const yamlDoc = documents.getYamlDocument(doc); + + const node = yamlDoc.documents[0].getNodeFromOffset(13); + const result = writer.write(node); + expect(result).not.to.be.null; + expect(result).to.deep.equals(` location: some `); + }); +}); diff --git a/test/hover.test.ts b/test/hover.test.ts index a94f36397..b4293ac6c 100644 --- a/test/hover.test.ts +++ b/test/hover.test.ts @@ -579,6 +579,97 @@ Source: [${SCHEMA_ID}](file:///${SCHEMA_ID})` }); }); + describe('Hover on anyOf', () => { + it('should show all matched schemas in anyOf', async () => { + languageService.addSchema(SCHEMA_ID, { + title: 'The Root', + description: 'Root Object', + type: 'object', + properties: { + child: { + title: 'Child', + anyOf: [ + { + $ref: '#/definitions/FirstChoice', + }, + { + $ref: '#/definitions/SecondChoice', + }, + ], + }, + }, + required: ['child'], + additionalProperties: false, + definitions: { + FirstChoice: { + title: 'FirstChoice', + description: 'The first choice', + type: 'object', + properties: { + choice: { + title: 'Choice', + default: 'first', + enum: ['first'], + type: 'string', + }, + property_a: { + title: 'Property A', + type: 'string', + }, + }, + required: ['property_a'], + }, + SecondChoice: { + title: 'SecondChoice', + description: 'The second choice', + type: 'object', + properties: { + choice: { + title: 'Choice', + default: 'second', + enum: ['second'], + type: 'string', + }, + property_b: { + title: 'Property B', + type: 'string', + }, + }, + required: ['property_b'], + }, + }, + }); + let content = 'ch|i|ld:'; + let result = await parseSetup(content); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `#### FirstChoice || SecondChoice\n\nThe first choice || The second choice\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + expect(telemetry.messages).to.be.empty; + + //use case 1: + content = 'ch|i|ld: \n property_a: test'; + result = await parseSetup(content); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `#### FirstChoice\n\nThe first choice\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + expect(telemetry.messages).to.be.empty; + + //use case 2: + content = 'ch|i|ld: \n property_b: test'; + result = await parseSetup(content); + assert.strictEqual(MarkupContent.is(result.contents), true); + assert.strictEqual( + (result.contents as MarkupContent).value, + `#### SecondChoice\n\nThe second choice\n\nSource: [${SCHEMA_ID}](file:///${SCHEMA_ID})` + ); + expect(telemetry.messages).to.be.empty; + }); + }); + describe('Bug fixes', () => { it('should convert binary data correctly', async () => { const content = diff --git a/test/schemaValidation.test.ts b/test/schemaValidation.test.ts index 76288e7e4..2c166bb5f 100644 --- a/test/schemaValidation.test.ts +++ b/test/schemaValidation.test.ts @@ -1308,7 +1308,25 @@ obj: 16, DiagnosticSeverity.Error, 'yaml-schema: Drone CI configuration file', - 'https://json.schemastore.org/drone' + 'https://json.schemastore.org/drone', + { + properties: [ + 'type', + 'environment', + 'steps', + 'volumes', + 'services', + 'image_pull_secrets', + 'node', + 'concurrency', + 'name', + 'platform', + 'workspace', + 'clone', + 'trigger', + 'depends_on', + ], + } ) ); }); @@ -1587,6 +1605,35 @@ obj: const result = await parseSetup(content); expect(result.length).to.eq(1); expect(result[0].message).to.eq('Property prop2 is not allowed.'); + expect((result[0].data as { properties: unknown })?.properties).to.deep.eq(['prop1']); + }); + + it('should return additional prop error when there is unknown prop - suggest missing props)', async () => { + const schema = { + type: 'object', + properties: { + prop1: { + type: 'string', + }, + prop2: { + type: 'string', + }, + }, + }; + languageService.addSchema(SCHEMA_ID, schema); + const content = `prop1: value1\npropX: you should not be there 'propX'`; + const result = await parseSetup(content); + expect( + result.map((r) => ({ + message: r.message, + properties: (r.data as { properties: unknown })?.properties, + })) + ).to.deep.eq([ + { + message: 'Property propX is not allowed.', + properties: ['prop2'], + }, + ]); }); it('should allow additional props on object when additionalProp is true on object', async () => { @@ -1617,15 +1664,12 @@ obj: }, }; languageService.addSchema(SCHEMA_ID, schema); - const content = `env: \${{ matrix.env1 }}`; + const content = `env: \${{ matrix.env1 }`; const result = await parseSetup(content); expect(result).to.be.not.empty; expect(telemetry.messages).to.be.empty; expect(result.length).to.eq(1); - assert.deepStrictEqual( - result[0].message, - 'String does not match the pattern of "^\\$\\{\\{\\s*fromJSON\\(.*\\)\\s*\\}\\}$".' - ); + assert.deepStrictEqual(result[0].message, 'String does not match the pattern of "^.*\\$\\{\\{(.|[\r\n])*\\}\\}.*$".'); }); it('should handle not valid schema object', async () => { @@ -1693,7 +1737,7 @@ obj: }); describe('Enum tests', () => { - it('Enum Validation', async () => { + it('Enum Validation with invalid enum value', async () => { languageService.addSchema(SCHEMA_ID, { type: 'object', properties: { @@ -1712,5 +1756,107 @@ obj: expect(result.length).to.eq(2); expect(telemetry.messages).to.be.empty; }); + + it('Enum Validation with invalid type', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + first: { + type: 'string', + enum: ['a', 'b'], + }, + second: { + type: 'number', + enum: [1, 2], + }, + }, + }); + const content = 'first: c\nsecond: a'; + const result = await parseSetup(content); + expect(result.length).to.eq(3); + expect(telemetry.messages).to.be.empty; + }); + + it('Enum Validation with invalid data', async () => { + languageService.addSchema(SCHEMA_ID, { + definitions: { + rule: { + description: 'A rule', + type: 'object', + properties: { + kind: { + description: 'The kind of rule', + type: 'string', + enum: ['tested'], + }, + }, + required: ['kind'], + additionalProperties: false, + }, + }, + properties: { + rules: { + description: 'Rule list', + type: 'array', + items: { + $ref: '#/definitions/rule', + }, + minProperties: 1, + additionalProperties: false, + }, + }, + }); + const content = 'rules:\n - kind: test'; + const result = await parseSetup(content); + expect(result.length).to.eq(1); + expect(result[0].message).to.eq('Value is not accepted. Valid values: "tested".'); + }); + + it('value matches more than one schema in oneOf - but among one is format matches', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + repository: { + oneOf: [ + { + type: 'string', + format: 'uri', + }, + { + type: 'string', + pattern: '^@', + }, + ], + }, + }, + }); + const content = `repository: '@bittrr'`; + const result = await parseSetup(content); + expect(result.length).to.eq(0); + expect(telemetry.messages).to.be.empty; + }); + + it('value matches more than one schema in oneOf', async () => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + foo: {}, + bar: {}, + }, + oneOf: [ + { + required: ['foo'], + }, + { + required: ['bar'], + }, + ], + }); + const content = `foo: bar\nbar: baz`; + const result = await parseSetup(content); + expect(result.length).to.eq(1); + expect(result[0].message).to.eq('Matches multiple schemas when only one must validate.'); + expect(telemetry.messages).to.be.empty; + }); }); }); diff --git a/test/settingsHandlers.test.ts b/test/settingsHandlers.test.ts index e53f23657..eaddf4c8e 100644 --- a/test/settingsHandlers.test.ts +++ b/test/settingsHandlers.test.ts @@ -77,6 +77,39 @@ describe('Settings Handlers Tests', () => { expect(connection.client.register).calledOnce; }); + describe('Settings for YAML style should ', () => { + it(' reflect to the settings ', async () => { + const settingsHandler = new SettingsHandler( + connection, + (languageService as unknown) as LanguageService, + settingsState, + (validationHandler as unknown) as ValidationHandler, + {} as Telemetry + ); + workspaceStub.getConfiguration.resolves([{ style: { flowMapping: 'forbid', flowSequence: 'forbid' } }, {}, {}, {}, {}]); + + await settingsHandler.pullConfiguration(); + expect(settingsState.style).to.exist; + expect(settingsState.style.flowMapping).to.eqls('forbid'); + expect(settingsState.style.flowSequence).to.eqls('forbid'); + }); + it(' reflect default values if no settings given', async () => { + const settingsHandler = new SettingsHandler( + connection, + (languageService as unknown) as LanguageService, + settingsState, + (validationHandler as unknown) as ValidationHandler, + {} as Telemetry + ); + workspaceStub.getConfiguration.resolves([{}, {}, {}, {}, {}]); + + await settingsHandler.pullConfiguration(); + expect(settingsState.style).to.exist; + expect(settingsState.style.flowMapping).to.eqls('allow'); + expect(settingsState.style.flowSequence).to.eqls('allow'); + }); + }); + describe('Settings for file associations should ', () => { it('reflect to settings state', async () => { const settingsHandler = new SettingsHandler( diff --git a/test/utils/serviceSetup.ts b/test/utils/serviceSetup.ts index 9ed33b30f..92b770553 100644 --- a/test/utils/serviceSetup.ts +++ b/test/utils/serviceSetup.ts @@ -19,6 +19,8 @@ export class ServiceSetup { customTags: [], indentation: undefined, yamlVersion: '1.2', + flowMapping: 'allow', + flowSequence: 'allow', }; withValidate(): ServiceSetup { @@ -60,4 +62,13 @@ export class ServiceSetup { this.languageSettings.indentation = indentation; return this; } + withFlowMapping(mapping: 'allow' | 'forbid'): ServiceSetup { + this.languageSettings.flowMapping = mapping; + return this; + } + + withFlowSequence(sequence: 'allow' | 'forbid'): ServiceSetup { + this.languageSettings.flowSequence = sequence; + return this; + } } diff --git a/test/utils/verifyError.ts b/test/utils/verifyError.ts index 7d42d78db..423d5b995 100644 --- a/test/utils/verifyError.ts +++ b/test/utils/verifyError.ts @@ -25,7 +25,7 @@ export function createExpectedError( endCharacter: number, severity: DiagnosticSeverity = 1, source = 'YAML', - code = ErrorCode.Undefined + code: string | number = ErrorCode.Undefined ): Diagnostic { return Diagnostic.create(Range.create(startLine, startCharacter, endLine, endCharacter), message, severity, code, source); } @@ -38,10 +38,11 @@ export function createDiagnosticWithData( endCharacter: number, severity: DiagnosticSeverity = 1, source = 'YAML', - schemaUri: string | string[] + schemaUri: string | string[], + data: Record = {} ): Diagnostic { const diagnostic: Diagnostic = createExpectedError(message, startLine, startCharacter, endLine, endCharacter, severity, source); - diagnostic.data = { schemaUri: typeof schemaUri === 'string' ? [schemaUri] : schemaUri }; + diagnostic.data = { schemaUri: typeof schemaUri === 'string' ? [schemaUri] : schemaUri, ...data }; return diagnostic; } diff --git a/test/yamlCodeActions.test.ts b/test/yamlCodeActions.test.ts index 298082f35..64259f6f6 100644 --- a/test/yamlCodeActions.test.ts +++ b/test/yamlCodeActions.test.ts @@ -11,6 +11,7 @@ import { CodeAction, CodeActionContext, Command, + DiagnosticSeverity, Range, TextDocumentIdentifier, TextEdit, @@ -183,4 +184,32 @@ describe('CodeActions Tests', () => { expect(result[0].edit.changes[TEST_URI]).deep.equal([TextEdit.del(Range.create(0, 5, 0, 13))]); }); }); + + describe('Convert to Block Style', () => { + it(' should generate action to convert flow map to block map ', () => { + const yaml = `host: phl-42 +datacenter: {location: canada , cab: 15} +animals: [dog , cat , mouse] `; + const doc = setupTextDocument(yaml); + const diagnostics = [ + createExpectedError('Flow style mapping is forbidden', 1, 12, 1, 39, DiagnosticSeverity.Error, 'YAML', 'flowMap'), + createExpectedError('Flow style sequence is forbidden', 2, 9, 2, 27, DiagnosticSeverity.Error, 'YAML', 'flowSeq'), + ]; + const params: CodeActionParams = { + context: CodeActionContext.create(diagnostics), + range: undefined, + textDocument: TextDocumentIdentifier.create(TEST_URI), + }; + const actions = new YamlCodeActions(clientCapabilities); + const result = actions.getCodeAction(doc, params); + expect(result).to.be.not.empty; + expect(result).to.have.lengthOf(2); + expect(result[0].edit.changes[TEST_URI]).deep.equal([ + TextEdit.replace(Range.create(1, 12, 1, 39), `\n location: canada \n cab: 15`), + ]); + expect(result[1].edit.changes[TEST_URI]).deep.equal([ + TextEdit.replace(Range.create(2, 9, 2, 27), `\n - dog \n - cat \n - mouse`), + ]); + }); + }); }); diff --git a/test/yamlCodeLens.test.ts b/test/yamlCodeLens.test.ts index 9e0ee81d0..32797f9c2 100644 --- a/test/yamlCodeLens.test.ts +++ b/test/yamlCodeLens.test.ts @@ -104,6 +104,21 @@ describe('YAML CodeLens', () => { ); }); + it('command name should contains schema title and description', async () => { + const doc = setupTextDocument('foo: bar'); + const schema = { + url: 'some://url/to/schema.json', + title: 'fooBar', + description: 'fooBarDescription', + } as JSONSchema; + yamlSchemaService.getSchemaForResource.resolves({ schema }); + const codeLens = new YamlCodeLens((yamlSchemaService as unknown) as YAMLSchemaService, telemetry); + const result = await codeLens.getCodeLens(doc); + expect(result[0].command).is.deep.equal( + createCommand('fooBar - fooBarDescription (schema.json)', YamlCommands.JUMP_TO_SCHEMA, 'some://url/to/schema.json') + ); + }); + it('should provide lens for oneOf schemas', async () => { const doc = setupTextDocument('foo: bar'); const schema = { diff --git a/test/yamlValidation.test.ts b/test/yamlValidation.test.ts index 227eda011..3c63e72c3 100644 --- a/test/yamlValidation.test.ts +++ b/test/yamlValidation.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Diagnostic } from 'vscode-languageserver-types'; +import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-types'; import { ValidationHandler } from '../src/languageserver/handlers/validationHandlers'; import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings'; import { ServiceSetup } from './utils/serviceSetup'; @@ -92,4 +92,85 @@ some: ]); }); }); + + describe(`YAML styles test`, () => { + it('should not report flow style', async () => { + const yaml = `host: phl-42 +datacenter: + location: canada + cab: 15 +animals: + - dog + - cat + - mouse`; + const result = await parseSetup(yaml); + expect(result).to.be.empty; + }); + it('should report flow style', async () => { + const yaml = `host: phl-42 +datacenter: {location: canada , cab: 15} +animals: [dog , cat , mouse] `; + + yamlSettings.style = { + flowMapping: 'forbid', + flowSequence: 'forbid', + }; + languageSettingsSetup = new ServiceSetup().withValidate().withFlowMapping('forbid').withFlowSequence('forbid'); + const { validationHandler: valHandler, yamlSettings: settings } = setupLanguageService( + languageSettingsSetup.languageSettings + ); + validationHandler = valHandler; + yamlSettings = settings; + const result = await parseSetup(yaml); + expect(result).not.to.be.empty; + expect(result.length).to.be.equal(2); + expect(result).to.include.deep.members([ + createExpectedError('Flow style mapping is forbidden', 1, 12, 1, 42, DiagnosticSeverity.Error, 'YAML', 'flowMap'), + createExpectedError('Flow style sequence is forbidden', 2, 9, 2, 28, DiagnosticSeverity.Error, 'YAML', 'flowSeq'), + ]); + }); + + it('should report only sequence when flow mapping is allow', async () => { + const yaml = `host: phl-42 +datacenter: {location: canada , cab: 15} +animals: [dog , cat , mouse] `; + + yamlSettings.style = { + flowMapping: 'forbid', + flowSequence: 'forbid', + }; + languageSettingsSetup = new ServiceSetup().withValidate().withFlowMapping('allow').withFlowSequence('forbid'); + const { validationHandler: valHandler, yamlSettings: settings } = setupLanguageService( + languageSettingsSetup.languageSettings + ); + validationHandler = valHandler; + yamlSettings = settings; + const result = await parseSetup(yaml); + expect(result).not.to.be.empty; + expect(result.length).to.be.equal(1); + expect(result).to.include.deep.members([ + createExpectedError('Flow style sequence is forbidden', 2, 9, 2, 28, DiagnosticSeverity.Error, 'YAML', 'flowSeq'), + ]); + }); + it('should report flow error for empty map & sequence', async () => { + const yaml = 'object: {} \nobject2: []'; + yamlSettings.style = { + flowMapping: 'forbid', + flowSequence: 'forbid', + }; + languageSettingsSetup = new ServiceSetup().withValidate().withFlowMapping('forbid').withFlowSequence('forbid'); + const { validationHandler: valHandler, yamlSettings: settings } = setupLanguageService( + languageSettingsSetup.languageSettings + ); + validationHandler = valHandler; + yamlSettings = settings; + const result = await parseSetup(yaml); + expect(result).not.to.be.empty; + expect(result.length).to.be.equal(2); + expect(result).to.include.deep.members([ + createExpectedError('Flow style mapping is forbidden', 0, 8, 0, 11, DiagnosticSeverity.Error, 'YAML', 'flowMap'), + createExpectedError('Flow style sequence is forbidden', 1, 9, 1, 10, DiagnosticSeverity.Error, 'YAML', 'flowSeq'), + ]); + }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 8585a7a26..5294b2662 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,13 +3,14 @@ "alwaysStrict": true, "declaration": true, "forceConsistentCasingInFileNames": true, - "lib": ["es2016", "WebWorker"], + "lib": ["es2020", "WebWorker"], "module": "commonjs", "moduleResolution": "node", "outDir": "./out/server", "sourceMap": true, - "target": "es6", - "allowSyntheticDefaultImports": true + "target": "es2020", + "allowSyntheticDefaultImports": true, + "skipLibCheck": true }, "include": [ "src", "test" ], "exclude": ["node_modules", "out"] diff --git a/yarn.lock b/yarn.lock index 20b766201..084c1f522 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,13 +10,6 @@ "@jridgewell/gen-mapping" "^0.1.0" "@jridgewell/trace-mapping" "^0.3.9" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.16.7": version "7.16.7" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz" @@ -145,7 +138,7 @@ "@babel/traverse" "^7.17.9" "@babel/types" "^7.17.0" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.16.7": +"@babel/highlight@^7.16.7": version "7.17.12" resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.17.12.tgz" integrity sha512-7yykMVF3hfZY2jsHZEEgLc+3x4o1O+fYyULu11GynEUQNwB6lua+IIQn1FiJxNucd5UlyJryrwsOh8PL9Sn8Qg== @@ -204,31 +197,41 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint/eslintrc@^1.3.2": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.2.tgz#58b69582f3b7271d8fa67fe5251767a5b38ea356" + integrity sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.4.0" + globals "^13.15.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== +"@humanwhocodes/config-array@^0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.5.tgz#bb679745224745fff1e9a41961c1d45a49f81c04" + integrity sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug== dependencies: - "@humanwhocodes/object-schema" "^1.2.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" minimatch "^3.0.4" -"@humanwhocodes/object-schema@^1.2.0": +"@humanwhocodes/gitignore-to-minimatch@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" + integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== @@ -383,10 +386,10 @@ resolved "https://registry.npmjs.org/@types/mocha/-/mocha-8.2.2.tgz" integrity sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw== -"@types/node@^12.11.7": - version "12.20.55" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" - integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@16.x": + version "16.11.60" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.60.tgz#a1fbca80c18dd80c8783557304cdb7d55ac3aff5" + integrity sha512-kYIYa1D1L+HDv5M5RXQeEu1o0FKA6yedZIoyugm/MBPROkLpX4L7HRxMrPVyo8bnvjpW/wDlqFNGzXNMb7AdRw== "@types/prettier@2.0.2": version "2.0.2" @@ -420,84 +423,84 @@ resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz#bf2e02a3dbd4aecaf95942ecd99b7402e03fad5e" integrity sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA== -"@typescript-eslint/eslint-plugin@^5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.0.tgz#524a11e15c09701733033c96943ecf33f55d9ca1" - integrity sha512-lvhRJ2pGe2V9MEU46ELTdiHgiAFZPKtLhiU5wlnaYpMc2+c1R8fh8i80ZAa665drvjHKUJyRRGg3gEm1If54ow== +"@typescript-eslint/eslint-plugin@^5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.0.tgz#ac919a199548861012e8c1fb2ec4899ac2bc22ae" + integrity sha512-GgHi/GNuUbTOeoJiEANi0oI6fF3gBQc3bGFYj40nnAPCbhrtEDf2rjBmefFadweBmO1Du1YovHeDP2h5JLhtTQ== dependencies: - "@typescript-eslint/scope-manager" "5.30.0" - "@typescript-eslint/type-utils" "5.30.0" - "@typescript-eslint/utils" "5.30.0" + "@typescript-eslint/scope-manager" "5.38.0" + "@typescript-eslint/type-utils" "5.38.0" + "@typescript-eslint/utils" "5.38.0" debug "^4.3.4" - functional-red-black-tree "^1.0.1" ignore "^5.2.0" regexpp "^3.2.0" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@^5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.30.0.tgz#a2184fb5f8ef2bf1db0ae61a43907e2e32aa1b8f" - integrity sha512-2oYYUws5o2liX6SrFQ5RB88+PuRymaM2EU02/9Ppoyu70vllPnHVO7ioxDdq/ypXHA277R04SVjxvwI8HmZpzA== +"@typescript-eslint/parser@^5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.38.0.tgz#5a59a1ff41a7b43aacd1bb2db54f6bf1c02b2ff8" + integrity sha512-/F63giJGLDr0ms1Cr8utDAxP2SPiglaD6V+pCOcG35P2jCqdfR7uuEhz1GIC3oy4hkUF8xA1XSXmd9hOh/a5EA== dependencies: - "@typescript-eslint/scope-manager" "5.30.0" - "@typescript-eslint/types" "5.30.0" - "@typescript-eslint/typescript-estree" "5.30.0" + "@typescript-eslint/scope-manager" "5.38.0" + "@typescript-eslint/types" "5.38.0" + "@typescript-eslint/typescript-estree" "5.38.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.30.0.tgz#bf585ee801ab4ad84db2f840174e171a6bb002c7" - integrity sha512-3TZxvlQcK5fhTBw5solQucWSJvonXf5yua5nx8OqK94hxdrT7/6W3/CS42MLd/f1BmlmmbGEgQcTHHCktUX5bQ== +"@typescript-eslint/scope-manager@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.38.0.tgz#8f0927024b6b24e28671352c93b393a810ab4553" + integrity sha512-ByhHIuNyKD9giwkkLqzezZ9y5bALW8VNY6xXcP+VxoH4JBDKjU5WNnsiD4HJdglHECdV+lyaxhvQjTUbRboiTA== dependencies: - "@typescript-eslint/types" "5.30.0" - "@typescript-eslint/visitor-keys" "5.30.0" + "@typescript-eslint/types" "5.38.0" + "@typescript-eslint/visitor-keys" "5.38.0" -"@typescript-eslint/type-utils@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.30.0.tgz#98f3af926a5099153f092d4dad87148df21fbaae" - integrity sha512-GF8JZbZqSS+azehzlv/lmQQ3EU3VfWYzCczdZjJRxSEeXDQkqFhCBgFhallLDbPwQOEQ4MHpiPfkjKk7zlmeNg== +"@typescript-eslint/type-utils@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.38.0.tgz#c8b7f681da825fcfc66ff2b63d70693880496876" + integrity sha512-iZq5USgybUcj/lfnbuelJ0j3K9dbs1I3RICAJY9NZZpDgBYXmuUlYQGzftpQA9wC8cKgtS6DASTvF3HrXwwozA== dependencies: - "@typescript-eslint/utils" "5.30.0" + "@typescript-eslint/typescript-estree" "5.38.0" + "@typescript-eslint/utils" "5.38.0" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.30.0.tgz#db7d81d585a3da3801432a9c1d2fafbff125e110" - integrity sha512-vfqcBrsRNWw/LBXyncMF/KrUTYYzzygCSsVqlZ1qGu1QtGs6vMkt3US0VNSQ05grXi5Yadp3qv5XZdYLjpp8ag== +"@typescript-eslint/types@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.38.0.tgz#8cd15825e4874354e31800dcac321d07548b8a5f" + integrity sha512-HHu4yMjJ7i3Cb+8NUuRCdOGu2VMkfmKyIJsOr9PfkBVYLYrtMCK/Ap50Rpov+iKpxDTfnqvDbuPLgBE5FwUNfA== -"@typescript-eslint/typescript-estree@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.0.tgz#4565ee8a6d2ac368996e20b2344ea0eab1a8f0bb" - integrity sha512-hDEawogreZB4n1zoqcrrtg/wPyyiCxmhPLpZ6kmWfKF5M5G0clRLaEexpuWr31fZ42F96SlD/5xCt1bT5Qm4Nw== +"@typescript-eslint/typescript-estree@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.0.tgz#89f86b2279815c6fb7f57d68cf9b813f0dc25d98" + integrity sha512-6P0RuphkR+UuV7Avv7MU3hFoWaGcrgOdi8eTe1NwhMp2/GjUJoODBTRWzlHpZh6lFOaPmSvgxGlROa0Sg5Zbyg== dependencies: - "@typescript-eslint/types" "5.30.0" - "@typescript-eslint/visitor-keys" "5.30.0" + "@typescript-eslint/types" "5.38.0" + "@typescript-eslint/visitor-keys" "5.38.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.30.0.tgz#1dac771fead5eab40d31860716de219356f5f754" - integrity sha512-0bIgOgZflLKIcZsWvfklsaQTM3ZUbmtH0rJ1hKyV3raoUYyeZwcjQ8ZUJTzS7KnhNcsVT1Rxs7zeeMHEhGlltw== +"@typescript-eslint/utils@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.38.0.tgz#5b31f4896471818153790700eb02ac869a1543f4" + integrity sha512-6sdeYaBgk9Fh7N2unEXGz+D+som2QCQGPAf1SxrkEr+Z32gMreQ0rparXTNGRRfYUWk/JzbGdcM8NSSd6oqnTA== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.30.0" - "@typescript-eslint/types" "5.30.0" - "@typescript-eslint/typescript-estree" "5.30.0" + "@typescript-eslint/scope-manager" "5.38.0" + "@typescript-eslint/types" "5.38.0" + "@typescript-eslint/typescript-estree" "5.38.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.30.0": - version "5.30.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.0.tgz#07721d23daca2ec4c2da7f1e660d41cd78bacac3" - integrity sha512-6WcIeRk2DQ3pHKxU1Ni0qMXJkjO/zLjBymlYBy/53qxe7yjEFSvzKLDToJjURUhSl2Fzhkl4SMXQoETauF74cw== +"@typescript-eslint/visitor-keys@5.38.0": + version "5.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.0.tgz#60591ca3bf78aa12b25002c0993d067c00887e34" + integrity sha512-MxnrdIyArnTi+XyFLR+kt/uNAcdOnmT+879os7qDRI+EYySR4crXJq9BXPfRzzLGq0wgxkwidrCJ9WCAoacm1w== dependencies: - "@typescript-eslint/types" "5.30.0" + "@typescript-eslint/types" "5.38.0" eslint-visitor-keys "^3.3.0" "@ungap/promise-all-settled@1.1.2": @@ -505,7 +508,7 @@ resolved "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz" integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -515,16 +518,16 @@ acorn-walk@^8.1.1: resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.4.0: - version "7.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - acorn@^8.4.1: version "8.7.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== +acorn@^8.8.0: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + agent-base@6: version "6.0.2" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" @@ -550,7 +553,7 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.1, ajv@^8.11.0: +ajv@^8.11.0: version "8.11.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== @@ -560,7 +563,7 @@ ajv@^8.0.1, ajv@^8.11.0: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@4.1.1, ansi-colors@^4.1.1: +ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== @@ -664,11 +667,6 @@ assertion-error@^1.1.0: resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -937,7 +935,7 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@4, debug@4.3.3, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@4, debug@4.3.3, debug@^4.1.0, debug@^4.1.1: version "4.3.3" resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -958,7 +956,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.3.4: +debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1056,13 +1054,6 @@ emoji-regex@^8.0.0: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: version "1.20.1" resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.1.tgz" @@ -1128,12 +1119,10 @@ escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -eslint-config-prettier@^6.11.0: - version "6.15.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.15.0.tgz" - integrity sha512-a1+kOYLR8wMGustcgAjdydMsQ2A/2ipRPwRKUmfYaSxc9ZPcrku080Ctl6zrZzZNs/U82MjSv+qKREkoq3bJaw== - dependencies: - get-stdin "^6.0.0" +eslint-config-prettier@^8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -1170,10 +1159,10 @@ eslint-plugin-import@^2.26.0: resolve "^1.22.0" tsconfig-paths "^3.14.1" -eslint-plugin-prettier@^3.1.4: - version "3.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5" - integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g== +eslint-plugin-prettier@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== dependencies: prettier-linter-helpers "^1.0.0" @@ -1185,12 +1174,13 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== dependencies: - eslint-visitor-keys "^1.1.0" + esrecurse "^4.3.0" + estraverse "^5.2.0" eslint-utils@^3.0.0: version "3.0.0" @@ -1199,11 +1189,6 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== - eslint-visitor-keys@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" @@ -1214,60 +1199,59 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@^7.2.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +eslint@^8.24.0: + version "8.24.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.24.0.tgz#489516c927a5da11b3979dbfb2679394523383c8" + integrity sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" + "@eslint/eslintrc" "^1.3.2" + "@humanwhocodes/config-array" "^0.10.5" + "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" + "@humanwhocodes/module-importer" "^1.0.1" ajv "^6.10.0" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.4.0" esquery "^1.4.0" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" + find-up "^5.0.0" + glob-parent "^6.0.1" + globals "^13.15.0" + globby "^11.1.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" import-fresh "^3.0.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + js-sdsl "^4.1.4" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" + regexpp "^3.2.0" + strip-ansi "^6.0.1" strip-json-comments "^3.1.0" - table "^6.0.9" text-table "^0.2.0" - v8-compile-cache "^2.0.3" -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a" + integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" esprima@^4.0.0: version "4.0.1" @@ -1379,9 +1363,9 @@ find-cache-dir@^3.2.0: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@5.0.0: +find-up@5.0.0, find-up@^5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== dependencies: locate-path "^6.0.0" @@ -1472,11 +1456,6 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - functions-have-names@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" @@ -1511,11 +1490,6 @@ get-package-type@^0.1.0: resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" @@ -1538,6 +1512,13 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob@7.2.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" @@ -1555,10 +1536,10 @@ globals@^11.1.0: resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.15.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.15.0.tgz#38113218c907d2f7e98658af246cef8b77e90bac" - integrity sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog== +globals@^13.15.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== dependencies: type-fest "^0.20.2" @@ -1579,6 +1560,11 @@ graceful-fs@^4.1.15: resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + growl@1.10.5: version "1.10.5" resolved "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz" @@ -1682,11 +1668,6 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" @@ -1944,14 +1925,19 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +js-sdsl@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.1.4.tgz#78793c90f80e8430b7d8dc94515b6c77d98a26a6" + integrity sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@4.1.0: +js-yaml@4.1.0, js-yaml@^4.1.0: version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" @@ -2081,11 +2067,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz" - integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= - log-driver@^1.2.7: version "1.2.7" resolved "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz" @@ -2507,11 +2488,6 @@ process-on-spawn@^1.0.0: dependencies: fromentries "^1.2.0" -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - psl@^1.1.28: version "1.8.0" resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" @@ -2555,7 +2531,7 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.1.0, regexpp@^3.2.0: +regexpp@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== @@ -2671,13 +2647,6 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1: - version "7.3.5" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" @@ -2745,15 +2714,6 @@ slash@^3.0.0: resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - source-map-support@^0.5.19: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -2799,7 +2759,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2874,17 +2834,6 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -table@^6.0.9: - version "6.8.0" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" - integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" @@ -3001,10 +2950,10 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" -typescript@^4.7.0: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +typescript@^4.8.3: + version "4.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.3.tgz#d59344522c4bc464a65a730ac695007fdb66dd88" + integrity sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig== unbox-primitive@^1.0.2: version "1.0.2" @@ -3033,11 +2982,6 @@ v8-compile-cache-lib@^3.0.0: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - verror@1.10.0: version "1.10.0" resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" From 519f9845dade9a19cffae4242569d1a3fa3e9a6b Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 11 Jul 2023 12:17:29 +0200 Subject: [PATCH 04/14] fix: suggest hyphen in array --- .../services/yamlCompletion.ts | 9 ++--- test/autoCompletion.test.ts | 36 ++++++++++++++++++- test/defaultSnippets.test.ts | 13 +++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 99e061f0b..c8ce3eea3 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -910,13 +910,8 @@ export class YamlCompletion { if (index < s.schema.items.length) { this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, 'value'); } - } else if ( - typeof s.schema.items === 'object' && - (s.schema.items.type === 'object' || isAnyOfAllOfOneOfType(s.schema.items)) - ) { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } else { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value'); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } } } @@ -958,7 +953,7 @@ export class YamlCompletion { ); collector.add({ kind: this.getSuggestionKind(schema.type), - label: '- (array item) ' + (schemaType || index), + label: '- (array item) ' + ((schemaType || index) ?? ''), documentation: documentation, insertText: insertText, insertTextFormat: InsertTextFormat.Snippet, diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 78b14996d..f26901435 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1488,6 +1488,39 @@ describe('Auto Completion Tests', () => { .then(done, done); }); + it('Array of enum autocomplete on 2nd position without `-`', (done) => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + references: { + type: 'array', + items: { + enum: ['Test'], + }, + }, + }, + }); + const content = 'references:\n - Test\n |\n|'; + const completion = parseSetup(content); + completion + .then(function (result) { + assert.deepEqual( + result.items.map((i) => ({ label: i.label, insertText: i.insertText })), + [ + { + insertText: 'Test', + label: 'Test', + }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); + }) + .then(done, done); + }); + it('Array of objects autocomplete with 4 space indentation check', async () => { const languageSettingsSetup = new ServiceSetup().withCompletion().withIndentation(' '); languageService.configure(languageSettingsSetup.languageSettings); @@ -1926,7 +1959,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 3, `Expecting 3 items in completion but found ${result.items.length}`); const resultDoc2 = await parseSetup(content, content.length); - assert.equal(resultDoc2.items.length, 0, `Expecting no items in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items.length, 1, `Expecting 1 item in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items[0].label, '- (array item) '); }); it('should handle absolute path', async () => { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 76fa569ad..8358f40d5 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -91,9 +91,16 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, '- item1: $1\n item2: $2'); - assert.equal(result.items[0].label, 'My array item'); + assert.deepEqual( + result.items.map((i) => ({ insertText: i.insertText, label: i.label })), + [ + { insertText: '- item1: $1\n item2: $2', label: 'My array item' }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); }) .then(done, done); }); From d0872b1150cfe77762253c13c11781b31e1e254e Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 5 Sep 2023 13:28:03 +0200 Subject: [PATCH 05/14] fix: schemaProvider in test --- test/autoCompletion.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index f26901435..4998daca8 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1489,7 +1489,7 @@ describe('Auto Completion Tests', () => { }); it('Array of enum autocomplete on 2nd position without `-`', (done) => { - languageService.addSchema(SCHEMA_ID, { + schemaProvider.addSchema(SCHEMA_ID, { type: 'object', properties: { references: { From 0432be8aa35f9eeec05594aeb99994df77609c18 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 11 Jul 2023 12:17:29 +0200 Subject: [PATCH 06/14] fix: suggest hyphen in array --- .../services/yamlCompletion.ts | 9 ++--- test/autoCompletion.test.ts | 36 ++++++++++++++++++- test/defaultSnippets.test.ts | 13 +++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 99e061f0b..c8ce3eea3 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -910,13 +910,8 @@ export class YamlCompletion { if (index < s.schema.items.length) { this.addSchemaValueCompletions(s.schema.items[index], separatorAfter, collector, types, 'value'); } - } else if ( - typeof s.schema.items === 'object' && - (s.schema.items.type === 'object' || isAnyOfAllOfOneOfType(s.schema.items)) - ) { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } else { - this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value'); + this.addSchemaValueCompletions(s.schema.items, separatorAfter, collector, types, 'value', true); } } } @@ -958,7 +953,7 @@ export class YamlCompletion { ); collector.add({ kind: this.getSuggestionKind(schema.type), - label: '- (array item) ' + (schemaType || index), + label: '- (array item) ' + ((schemaType || index) ?? ''), documentation: documentation, insertText: insertText, insertTextFormat: InsertTextFormat.Snippet, diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 78b14996d..f26901435 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1488,6 +1488,39 @@ describe('Auto Completion Tests', () => { .then(done, done); }); + it('Array of enum autocomplete on 2nd position without `-`', (done) => { + languageService.addSchema(SCHEMA_ID, { + type: 'object', + properties: { + references: { + type: 'array', + items: { + enum: ['Test'], + }, + }, + }, + }); + const content = 'references:\n - Test\n |\n|'; + const completion = parseSetup(content); + completion + .then(function (result) { + assert.deepEqual( + result.items.map((i) => ({ label: i.label, insertText: i.insertText })), + [ + { + insertText: 'Test', + label: 'Test', + }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); + }) + .then(done, done); + }); + it('Array of objects autocomplete with 4 space indentation check', async () => { const languageSettingsSetup = new ServiceSetup().withCompletion().withIndentation(' '); languageService.configure(languageSettingsSetup.languageSettings); @@ -1926,7 +1959,8 @@ describe('Auto Completion Tests', () => { assert.equal(result.items.length, 3, `Expecting 3 items in completion but found ${result.items.length}`); const resultDoc2 = await parseSetup(content, content.length); - assert.equal(resultDoc2.items.length, 0, `Expecting no items in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items.length, 1, `Expecting 1 item in completion but found ${resultDoc2.items.length}`); + assert.equal(resultDoc2.items[0].label, '- (array item) '); }); it('should handle absolute path', async () => { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 76fa569ad..8358f40d5 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -91,9 +91,16 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content, content.length); completion .then(function (result) { - assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, '- item1: $1\n item2: $2'); - assert.equal(result.items[0].label, 'My array item'); + assert.deepEqual( + result.items.map((i) => ({ insertText: i.insertText, label: i.label })), + [ + { insertText: '- item1: $1\n item2: $2', label: 'My array item' }, + { + insertText: '- $1\n', + label: '- (array item) ', + }, + ] + ); }) .then(done, done); }); From 86a72edacd70b48285e4373aa7e43538285edfac Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 5 Sep 2023 13:28:03 +0200 Subject: [PATCH 07/14] fix: schemaProvider in test --- test/autoCompletion.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index f26901435..4998daca8 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1489,7 +1489,7 @@ describe('Auto Completion Tests', () => { }); it('Array of enum autocomplete on 2nd position without `-`', (done) => { - languageService.addSchema(SCHEMA_ID, { + schemaProvider.addSchema(SCHEMA_ID, { type: 'object', properties: { references: { From 8e4d05f1bf28c2bb7a6070f4aca3444bb047de0d Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Fri, 13 Oct 2023 13:03:53 +0200 Subject: [PATCH 08/14] fix: hyphen for array item --- src/languageservice/services/yamlCompletion.ts | 5 +++-- test/autoCompletion.test.ts | 4 ++-- test/defaultSnippets.test.ts | 15 ++++++++++++++- test/fixtures/defaultSnippets.json | 12 ++++++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index c8ce3eea3..bde50e8fb 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -814,7 +814,7 @@ export class YamlCompletion { collector, {}, 'property', - Array.isArray(nodeParent.items) + Array.isArray(nodeParent.items) && !isInArray ); } @@ -1424,10 +1424,11 @@ export class YamlCompletion { } else if (schema.enumDescriptions && i < schema.enumDescriptions.length) { documentation = schema.enumDescriptions[i]; } + const insertText = (isArray ? '- ' : '') + this.getInsertTextForValue(enm, separatorAfter, schema.type); collector.add({ kind: this.getSuggestionKind(schema.type), label: this.getLabelForValue(enm), - insertText: this.getInsertTextForValue(enm, separatorAfter, schema.type), + insertText, insertTextFormat: InsertTextFormat.Snippet, documentation: documentation, }); diff --git a/test/autoCompletion.test.ts b/test/autoCompletion.test.ts index 4998daca8..dfc5d78a3 100644 --- a/test/autoCompletion.test.ts +++ b/test/autoCompletion.test.ts @@ -1488,7 +1488,7 @@ describe('Auto Completion Tests', () => { .then(done, done); }); - it('Array of enum autocomplete on 2nd position without `-`', (done) => { + it('Array of enum autocomplete on 2nd position without `-` should auto add `-` and `- (array item)`', (done) => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', properties: { @@ -1508,7 +1508,7 @@ describe('Auto Completion Tests', () => { result.items.map((i) => ({ label: i.label, insertText: i.insertText })), [ { - insertText: 'Test', + insertText: '- Test', // auto added `- ` label: 'Test', }, { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 8358f40d5..36a1cadbd 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -240,6 +240,19 @@ describe('Default Snippet Tests', () => { .then(done, done); }); + it('Snippet in string schema should autocomplete on same line (snippet is defined in body property)', (done) => { + const content = 'arrayStringValueSnippet:\n - |\n|'; + const completion = parseSetup(content); + completion + .then(function (result) { + assert.deepEqual( + result.items.map((i) => ({ label: i.label, insertText: i.insertText })), + [{ insertText: 'banana', label: 'Banana' }] + ); + }) + .then(done, done); + }); + it('Snippet in boolean schema should autocomplete on same line', (done) => { const content = 'boolean: | |'; // len: 10, pos: 9 const completion = parseSetup(content); @@ -273,7 +286,7 @@ describe('Default Snippet Tests', () => { const completion = parseSetup(content); completion .then(function (result) { - assert.equal(result.items.length, 15); // This is just checking the total number of snippets in the defaultSnippets.json + assert.equal(result.items.length, 16); // This is just checking the total number of snippets in the defaultSnippets.json assert.equal(result.items[4].label, 'longSnippet'); // eslint-disable-next-line assert.equal( diff --git a/test/fixtures/defaultSnippets.json b/test/fixtures/defaultSnippets.json index 5d4b69d2a..42ff75d06 100644 --- a/test/fixtures/defaultSnippets.json +++ b/test/fixtures/defaultSnippets.json @@ -110,6 +110,18 @@ } ] }, + "arrayStringValueSnippet": { + "type": "array", + "items": { + "type": "string", + "defaultSnippets": [ + { + "label": "Banana", + "body": "banana" + } + ] + } + }, "arrayObjectSnippet": { "type": "object", "defaultSnippets": [ From e7ad86793e718975953fd854c7bb43cd17a5b726 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 11 Mar 2025 12:27:18 +0100 Subject: [PATCH 09/14] feat: property snippets --- .../services/yamlCompletion.ts | 97 +++- src/languageservice/utils/json.ts | 2 +- src/languageservice/utils/strings.ts | 15 + test/autoCompletionFix.test.ts | 536 ++++++++++++++++++ test/defaultSnippets.test.ts | 8 +- test/strings.test.ts | 83 ++- 6 files changed, 710 insertions(+), 31 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 5283b76a0..4a525360a 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -37,6 +37,7 @@ import { indexOf, isInComment, isMapContainsEmptyPair } from '../utils/astUtils' import { isModeline } from './modelineUtil'; import { getSchemaTypeName, isAnyOfAllOfOneOfType, isPrimitiveType } from '../utils/schemaUtils'; import { YamlNode } from '../jsonASTTypes'; +import { addIndentationToMultilineString } from '../utils/strings'; const localize = nls.loadMessageBundle(); @@ -523,7 +524,7 @@ export class YamlCompletion { collector.add({ kind: CompletionItemKind.Property, label: currentWord, - insertText: this.getInsertTextForProperty(currentWord, null, ''), + insertText: this.getInsertTextForProperty(currentWord, null, '', collector), insertTextFormat: InsertTextFormat.Snippet, }); } @@ -760,6 +761,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, identCompensation + this.indentation ); } @@ -787,6 +789,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, + collector, identCompensation + this.indentation ), insertTextFormat: InsertTextFormat.Snippet, @@ -944,7 +947,7 @@ export class YamlCompletion { index?: number ): void { const schemaType = getSchemaTypeName(schema); - const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter).insertText.trimLeft()}`; + const insertText = `- ${this.getInsertTextForObject(schema, separatorAfter, collector).insertText.trimLeft()}`; //append insertText to documentation const schemaTypeTitle = schemaType ? ' type `' + schemaType + '`' : ''; const schemaDescription = schema.description ? ' (' + schema.description + ')' : ''; @@ -965,6 +968,7 @@ export class YamlCompletion { key: string, propertySchema: JSONSchema, separatorAfter: string, + collector: CompletionsCollector, indent = this.indentation ): string { const propertyText = this.getInsertTextForValue(key, '', 'string'); @@ -1035,11 +1039,11 @@ export class YamlCompletion { nValueProposals += propertySchema.examples.length; } if (propertySchema.properties) { - return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, indent).insertText}`; + return `${resultText}\n${this.getInsertTextForObject(propertySchema, separatorAfter, collector, indent).insertText}`; } else if (propertySchema.items) { - return `${resultText}\n${indent}- ${ - this.getInsertTextForArray(propertySchema.items, separatorAfter, 1, indent).insertText - }`; + let insertText = this.getInsertTextForArray(propertySchema.items, separatorAfter, collector, 1, indent).insertText; + insertText = resultText + addIndentationToMultilineString(insertText, `\n${indent}- `, ' '); + return insertText; } if (nValueProposals === 0) { switch (type) { @@ -1079,10 +1083,30 @@ export class YamlCompletion { private getInsertTextForObject( schema: JSONSchema, separatorAfter: string, + collector: CompletionsCollector, indent = this.indentation, insertIndex = 1 ): InsertText { let insertText = ''; + if (Array.isArray(schema.defaultSnippets) && schema.defaultSnippets.length === 1) { + const body = schema.defaultSnippets[0].body; + if (isDefined(body)) { + let value = this.getInsertTextForSnippetValue( + body, + '', + { + newLineFirst: false, + indentFirstObject: false, + shouldIndentWithTab: false, + }, + Object.keys(collector.proposed), + 0 + ); + value = addIndentationToMultilineString(value, indent, indent); + + return { insertText: value, insertIndex }; + } + } if (!schema.properties) { insertText = `${indent}$${insertIndex++}\n`; return { insertText, insertIndex }; @@ -1122,18 +1146,22 @@ export class YamlCompletion { } case 'array': { - const arrayInsertResult = this.getInsertTextForArray(propertySchema.items, separatorAfter, insertIndex++, indent); - const arrayInsertLines = arrayInsertResult.insertText.split('\n'); - let arrayTemplate = arrayInsertResult.insertText; - if (arrayInsertLines.length > 1) { - for (let index = 1; index < arrayInsertLines.length; index++) { - const element = arrayInsertLines[index]; - arrayInsertLines[index] = ` ${element}`; - } - arrayTemplate = arrayInsertLines.join('\n'); - } + const arrayInsertResult = this.getInsertTextForArray( + propertySchema.items, + separatorAfter, + collector, + insertIndex++, + indent + ); + insertIndex = arrayInsertResult.insertIndex; - insertText += `${indent}${key}:\n${indent}${this.indentation}- ${arrayTemplate}\n`; + insertText += + `${indent}${key}:` + + addIndentationToMultilineString( + arrayInsertResult.insertText, + `\n${indent}${this.indentation}- `, + `${this.indentation} ` + ); } break; case 'object': @@ -1141,6 +1169,7 @@ export class YamlCompletion { const objectInsertResult = this.getInsertTextForObject( propertySchema, separatorAfter, + collector, `${indent}${this.indentation}`, insertIndex++ ); @@ -1176,8 +1205,14 @@ export class YamlCompletion { return { insertText, insertIndex }; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private getInsertTextForArray(schema: any, separatorAfter: string, insertIndex = 1, indent = this.indentation): InsertText { + private getInsertTextForArray( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + schema: any, + separatorAfter: string, + collector: CompletionsCollector, + insertIndex = 1, + indent = this.indentation + ): InsertText { let insertText = ''; if (!schema) { insertText = `$${insertIndex++}`; @@ -1205,7 +1240,7 @@ export class YamlCompletion { break; case 'object': { - const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, `${indent} `, insertIndex++); + const objectInsertResult = this.getInsertTextForObject(schema, separatorAfter, collector, indent, insertIndex++); insertText = objectInsertResult.insertText.trimLeft(); insertIndex = objectInsertResult.insertIndex; } @@ -1391,11 +1426,17 @@ export class YamlCompletion { hasProposals = true; }); } - this.collectDefaultSnippets(schema, separatorAfter, collector, { - newLineFirst: true, - indentFirstObject: true, - shouldIndentWithTab: true, - }); + this.collectDefaultSnippets( + schema, + separatorAfter, + collector, + { + newLineFirst: true, + indentFirstObject: true, + shouldIndentWithTab: true, + }, + arrayDepth + ); if (!hasProposals && typeof schema.items === 'object' && !Array.isArray(schema.items)) { this.addDefaultValueCompletions(schema.items, separatorAfter, collector, arrayDepth + 1); } @@ -1484,6 +1525,12 @@ export class YamlCompletion { if (insertText === '' && value) { continue; } + // detection of specific situation: snippets for value Completion + // snippets located inside schema.items and line without hyphen (value completion) + if (arrayDepth === 1 && !Array.isArray(value) && settings.newLineFirst) { + insertText = addIndentationToMultilineString(insertText.trimStart(), `\n${this.indentation}- `, ' '); + } + label = label || this.getLabelForSnippetValue(value); } else if (typeof s.bodyText === 'string') { let prefix = '', diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 00ef5483d..be76eb511 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -37,7 +37,7 @@ export function stringifyObject( let result = ''; for (let i = 0; i < obj.length; i++) { let pseudoObj = obj[i]; - if (typeof obj[i] !== 'object') { + if (typeof obj[i] !== 'object' || obj[i] === null) { result += '\n' + newIndent + '- ' + stringifyLiteral(obj[i]); continue; } diff --git a/src/languageservice/utils/strings.ts b/src/languageservice/utils/strings.ts index 6171411df..b0810e507 100644 --- a/src/languageservice/utils/strings.ts +++ b/src/languageservice/utils/strings.ts @@ -87,3 +87,18 @@ export function getFirstNonWhitespaceCharacterAfterOffset(str: string, offset: n } return offset; } + +export function addIndentationToMultilineString(text: string, firstIndent: string, nextIndent: string): string { + let wasFirstLineIndented = false; + return text.replace(/^.*$/gm, (match) => { + if (!match) { + return match; + } + // Add fistIndent to first line or if the previous line was empty + if (!wasFirstLineIndented) { + wasFirstLineIndented = true; + return firstIndent + match; + } + return nextIndent + match; // Add indent to other lines + }); +} diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index d8ec5b040..bdf825038 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -1207,6 +1207,542 @@ objB: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetBody']); }); + describe('variations of defaultSnippets', () => { + const getNestedSchema = (schema: JSONSchema['properties']): JSONSchema => { + return { + type: 'object', + properties: { + snippets: { + type: 'object', + properties: { + ...schema, + }, + }, + }, + }; + }; + + // STRING + describe('defaultSnippet for string property', () => { + const schema = getNestedSchema({ + snippetString: { + type: 'string', + defaultSnippets: [ + { + label: 'labelSnippetString', + body: 'value', + }, + ], + }, + }); + + it('should suggest defaultSnippet for STRING property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetStr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetString: value']); + }); + + it('should suggest defaultSnippet for STRING property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // STRING + + // OBJECT + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + required: ['item1'], + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1: 'value', + item2: { + item3: 'value nested', + }, + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value + item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: ` + item1: value + item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value with indent', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: `item1: value +item2: + item3: value nested`, + }, + { + label: 'item1', // key intellisense + insertText: 'item1: ', + }, + { + label: 'object', // parent intellisense + insertText: 'item1: ', + }, + ]); + }); + + it('should suggest partial defaultSnippet for OBJECT property - subset of items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', + insertText: `item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest no defaultSnippet for OBJECT property - all items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + item2: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([]); + }); + }); // OBJECT + + // OBJECT - Snippet nested + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { + type: 'object', + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1_1: 'value', + item1_2: { + item1_2_1: 'value nested', + }, + }, + }, + ], + }, + }, + required: ['item1'], + }, + }); + + it('should suggest defaultSnippet for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: + item1_1: value + item1_2: + item1_2_1: value nested`, + }, + ]); + }); + }); // OBJECT - Snippet nested + + // ARRAY + describe('defaultSnippet for ARRAY property', () => { + describe('defaultSnippets on the property level as an object value', () => { + const schema = getNestedSchema({ + snippetArray: { + type: 'array', + $comment: + 'property - Not implemented, OK value, OK value nested, OK value nested with -, OK on 2nd index without or with -', + items: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + }, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property (not implemented)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetArray', + insertText: 'snippetArray:\n - ', + }, + ]); + }); + + // this is hard to fix, it requires bigger refactor of `collectDefaultSnippets` function + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: ` + - item1: value + item2: value2`, + }, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + }); + describe('defaultSnippets on the items level as an object value', () => { + const schema = getNestedSchema({ + snippetArray2: { + type: 'array', + items: { + type: 'object', + additionalProperties: true, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetArray2:\n - item1: value\n item2: value2', + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + ` + - item1: value + item2: value2`, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + }); // ARRAY - Snippet on items level + + describe('defaultSnippets on the items level, ARRAY - Body is array of primitives', () => { + const schema = getNestedSchema({ + snippetArrayPrimitives: { + type: 'array', + items: { + type: ['string', 'boolean', 'number', 'null'], + defaultSnippets: [ + { + body: ['value', 5, null, false], + }, + ], + }, + }, + }); + + // fix if needed + // it('should suggest defaultSnippet for ARRAY property with primitives - unfinished property', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives|\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + // 'snippetArrayPrimitives:\n - value\n - 5\n - null\n - false', + // ]); + // }); + + it('should suggest defaultSnippet for ARRAY property with primitives - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayPrimitives: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value\n - 5\n - null\n - false']); + }); + + // skip, fix if needed + // it('should suggest defaultSnippet for ARRAY property with primitives - value with indent (with hyphen)', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives: + // - |\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n - 5\n - null\n - false']); + // }); + // it('should suggest defaultSnippet for ARRAY property with primitives - value on 2nd position', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives: + // - some other value + // - |\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['- value\n - 5\n - null\n - false']); + // }); + }); // ARRAY - Body is array of primitives + + describe('defaultSnippets on the items level, ARRAY - Body is string', () => { + const schema = getNestedSchema({ + snippetArrayString: { + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + body: 'value', + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property with string - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - ${1}']); + // better to suggest, fix if needed + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + it('should suggest defaultSnippet for ARRAY property with string - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - some other value + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // ARRAY - Body is simple string + }); // ARRAY + }); + describe('should suggest prop of the object (based on not completed prop name)', () => { const schema: JSONSchema = { definitions: { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 7ffaf9d8b..fbeff5c6e 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -163,7 +163,7 @@ describe('Default Snippet Tests', () => { assert.equal(result.items.length, 2); assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2'); assert.equal(result.items[0].label, 'Object item'); - assert.equal(result.items[1].insertText, 'key:\n '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -177,7 +177,7 @@ describe('Default Snippet Tests', () => { assert.notEqual(result.items.length, 0); assert.equal(result.items[0].insertText, 'key1: $1\nkey2: $2'); assert.equal(result.items[0].label, 'Object item'); - assert.equal(result.items[1].insertText, 'key:\n '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -191,7 +191,7 @@ describe('Default Snippet Tests', () => { assert.notEqual(result.items.length, 0); assert.equal(result.items[0].insertText, 'key1: '); assert.equal(result.items[0].label, 'Object item'); - assert.equal(result.items[1].insertText, 'key:\n '); + assert.equal(result.items[1].insertText, 'key:\n key1: '); assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -202,7 +202,7 @@ describe('Default Snippet Tests', () => { completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'key:\n '); + assert.equal(result.items[0].insertText, 'key:\n'); assert.equal(result.items[0].label, 'key'); }) .then(done, done); diff --git a/test/strings.test.ts b/test/strings.test.ts index 45741f7e9..4b808200d 100644 --- a/test/strings.test.ts +++ b/test/strings.test.ts @@ -2,7 +2,13 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { startsWith, endsWith, convertSimple2RegExp, safeCreateUnicodeRegExp } from '../src/languageservice/utils/strings'; +import { + startsWith, + endsWith, + convertSimple2RegExp, + safeCreateUnicodeRegExp, + addIndentationToMultilineString, +} from '../src/languageservice/utils/strings'; import * as assert from 'assert'; import { expect } from 'chai'; @@ -106,5 +112,80 @@ describe('String Tests', () => { const result = safeCreateUnicodeRegExp('^[\\w\\-_]+$'); expect(result).is.not.undefined; }); + + describe('addIndentationToMultilineString', () => { + it('should add indentation to a single line string', () => { + const text = 'hello'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' hello'); + }); + + it('should add indentation to a multiline string', () => { + const text = 'hello\nworld'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' hello\n world'); + }); + + it('should not indent empty string', () => { + const text = ''; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ''); + }); + + it('should not indent string with only newlines', () => { + const text = '\n\n'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n\n'); + }); + it('should not indent empty lines', () => { + const text = '\ntest\n'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n test\n'); + }); + + it('should handle string with multiple lines', () => { + const text = 'line1\nline2\nline3'; + const firstIndent = ' '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' line1\n line2\n line3'); + }); + + it('should handle string with multiple lines and tabs', () => { + const text = 'line1\nline2\nline3'; + const firstIndent = '\t'; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, ' line1\n line2\n line3'); + }); + + it('should prepare text for array snippet', () => { + const text = `obj: + prop1: value1 + prop2: value2`; + const firstIndent = '\n- '; + const nextIndent = ' '; + + const result = addIndentationToMultilineString(text, firstIndent, nextIndent); + assert.equal(result, '\n- obj:\n prop1: value1\n prop2: value2'); + }); + }); }); }); From d2d9bdd0a06f90b2fff4b6901185e44fe8928ce2 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Mon, 17 Mar 2025 11:17:44 +0100 Subject: [PATCH 10/14] fix: object property snippet - should not check existing props in the yaml - because it's properties from the parent object --- .../services/yamlCompletion.ts | 2 +- test/autoCompletionFix.test.ts | 19 +++++++++++++++++++ test/defaultSnippets.test.ts | 7 ++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index bcf4d4366..84fde7ea0 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1118,7 +1118,7 @@ export class YamlCompletion { indentFirstObject: false, shouldIndentWithTab: false, }, - Object.keys(collector.proposed), + [], 0 ); value = addIndentationToMultilineString(value, indent, indent); diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index d0f9c3df3..af5efc871 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -1328,6 +1328,25 @@ snippets: label: 'snippetObject', insertText: `snippetObject: item1: value + item2: + item3: value nested`, + }, + ]); + }); + it('should suggest defaultSnippet for OBJECT property - unfinished property, should keep all snippet properties', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + item1: value + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value item2: item3: value nested`, }, diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 46abe0ddf..b38a4dae3 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -184,14 +184,14 @@ describe('Default Snippet Tests', () => { }); it('Snippet in object schema should suggest some of the snippet props because some of them are already in the YAML', (done) => { - const content = 'object:\n key:\n key2: value\n '; + const content = 'object:\n key:\n key2: value\n '; // position is nested in `key` const completion = parseSetup(content, content.length); completion .then(function (result) { assert.notEqual(result.items.length, 0); assert.equal(result.items[0].insertText, 'key1: '); assert.equal(result.items[0].label, 'Object item'); - assert.equal(result.items[1].insertText, 'key:\n key1: '); + assert.equal(result.items[1].insertText, 'key:\n key1: $1\n key2: $2'); // recursive item (key inside key) assert.equal(result.items[1].label, 'key'); }) .then(done, done); @@ -202,7 +202,8 @@ describe('Default Snippet Tests', () => { completion .then(function (result) { assert.equal(result.items.length, 1); - assert.equal(result.items[0].insertText, 'key:\n'); + // snippet for nested `key` property + assert.equal(result.items[0].insertText, 'key:\n key1: $1\n key2: $2'); // recursive item (key inside key) assert.equal(result.items[0].label, 'key'); }) .then(done, done); From a9aa9cc69a7fc170a1082ed4546e8bb536a5573b Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 1 Apr 2025 10:53:49 +0200 Subject: [PATCH 11/14] fix: array snippets --- .../services/yamlCompletion.ts | 19 +- src/languageservice/utils/json.ts | 12 +- test/autoCompletionFix.test.ts | 571 -------------- test/defaultSnippets.test.ts | 699 +++++++++++++++++- 4 files changed, 719 insertions(+), 582 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index d2a8cc20e..3b98841e5 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1124,7 +1124,12 @@ export class YamlCompletion { [], 0 ); - value = addIndentationToMultilineString(value, indent, indent); + if (Array.isArray(body)) { + // hyphen will be added later, so remove it, indent is ok + value = value.replace(/^\n( *)- /, '$1'); + } else { + value = addIndentationToMultilineString(value, indent, indent); + } return { insertText: value, insertIndex }; } @@ -1455,9 +1460,9 @@ export class YamlCompletion { separatorAfter, collector, { - newLineFirst: !isArray, - indentFirstObject: !isArray, - shouldIndentWithTab: !isArray, + newLineFirst: !isArray && !collector.context.hasHyphen, + indentFirstObject: !isArray && !collector.context.hasHyphen, + shouldIndentWithTab: !isArray && !collector.context.hasHyphen, }, arrayDepth, isArray @@ -1534,8 +1539,14 @@ export class YamlCompletion { const existingProps = Object.keys(collector.proposed).filter( (proposedProp) => collector.proposed[proposedProp].label === existingProposeItem ); + insertText = this.getInsertTextForSnippetValue(value, separatorAfter, settings, existingProps); + if (collector.context.hasHyphen && Array.isArray(value)) { + // modify the array snippet if the line contains a hyphen + insertText = insertText.replace(/^\n( *)- /, '$1'); + } + // if snippet result is empty and value has a real value, don't add it as a completion if (insertText === '' && value) { continue; diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index be76eb511..43a1be2dd 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -34,6 +34,8 @@ export function stringifyObject( if (obj.length === 0) { return ''; } + // don't indent the first element of the primitive array + const newIndent = depth > 0 ? indent + settings.indentation : ''; let result = ''; for (let i = 0; i < obj.length; i++) { let pseudoObj = obj[i]; @@ -44,7 +46,15 @@ export function stringifyObject( if (!Array.isArray(obj[i])) { pseudoObj = prependToObject(obj[i], consecutiveArrays); } - result += stringifyObject(pseudoObj, indent, stringifyLiteral, settings, (depth += 1), consecutiveArrays); + result += stringifyObject( + pseudoObj, + indent, + stringifyLiteral, + // overwrite the settings for array, it's valid for object type - not array + { ...settings, newLineFirst: true, shouldIndentWithTab: false }, + depth, + consecutiveArrays + ); } return result; } else { diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 65628b36b..8084af08e 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -1240,577 +1240,6 @@ objB: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetBody']); }); - describe('variations of defaultSnippets', () => { - const getNestedSchema = (schema: JSONSchema['properties']): JSONSchema => { - return { - type: 'object', - properties: { - snippets: { - type: 'object', - properties: { - ...schema, - }, - }, - }, - }; - }; - - // STRING - describe('defaultSnippet for string property', () => { - const schema = getNestedSchema({ - snippetString: { - type: 'string', - defaultSnippets: [ - { - label: 'labelSnippetString', - body: 'value', - }, - ], - }, - }); - - it('should suggest defaultSnippet for STRING property - unfinished property', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetStr|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetString: value']); - }); - - it('should suggest defaultSnippet for STRING property - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetString: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); - }); - }); // STRING - - // OBJECT - describe('defaultSnippet for OBJECT property', () => { - const schema = getNestedSchema({ - snippetObject: { - type: 'object', - properties: { - item1: { type: 'string' }, - }, - required: ['item1'], - defaultSnippets: [ - { - label: 'labelSnippetObject', - body: { - item1: 'value', - item2: { - item3: 'value nested', - }, - }, - }, - ], - }, - }); - - it('should suggest defaultSnippet for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetOb|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'snippetObject', - insertText: `snippetObject: - item1: value - item2: - item3: value nested`, - }, - ]); - }); - it('should suggest defaultSnippet for OBJECT property - unfinished property, should keep all snippet properties', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - item1: value - snippetOb|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'snippetObject', - insertText: `snippetObject: - item1: value - item2: - item3: value nested`, - }, - ]); - }); - - it('should suggest defaultSnippet for OBJECT property - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetObject: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'labelSnippetObject', // snippet intellisense - insertText: ` - item1: value - item2: - item3: value nested`, - }, - ]); - }); - - it('should suggest defaultSnippet for OBJECT property - value with indent', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetObject: - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'labelSnippetObject', // snippet intellisense - insertText: `item1: value -item2: - item3: value nested`, - }, - { - label: 'item1', // key intellisense - insertText: 'item1: ', - }, - { - label: 'object', // parent intellisense - insertText: 'item1: ', - }, - ]); - }); - - it('should suggest partial defaultSnippet for OBJECT property - subset of items already there', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetObject: - item1: val - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'labelSnippetObject', - insertText: `item2: - item3: value nested`, - }, - ]); - }); - - it('should suggest no defaultSnippet for OBJECT property - all items already there', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetObject: - item1: val - item2: val - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([]); - }); - }); // OBJECT - - // OBJECT - Snippet nested - describe('defaultSnippet for OBJECT property', () => { - const schema = getNestedSchema({ - snippetObject: { - type: 'object', - properties: { - item1: { - type: 'object', - defaultSnippets: [ - { - label: 'labelSnippetObject', - body: { - item1_1: 'value', - item1_2: { - item1_2_1: 'value nested', - }, - }, - }, - ], - }, - }, - required: ['item1'], - }, - }); - - it('should suggest defaultSnippet for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetOb|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'snippetObject', - insertText: `snippetObject: - item1: - item1_1: value - item1_2: - item1_2_1: value nested`, - }, - ]); - }); - }); // OBJECT - Snippet nested - - // ARRAY - describe('defaultSnippet for ARRAY property', () => { - describe('defaultSnippets on the property level as an object value', () => { - const schema = getNestedSchema({ - snippetArray: { - type: 'array', - $comment: - 'property - Not implemented, OK value, OK value nested, OK value nested with -, OK on 2nd index without or with -', - items: { - type: 'object', - properties: { - item1: { type: 'string' }, - }, - }, - defaultSnippets: [ - { - label: 'labelSnippetArray', - body: { - item1: 'value', - item2: 'value2', - }, - }, - ], - }, - }); - - it('should suggest defaultSnippet for ARRAY property - unfinished property (not implemented)', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetAr|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'snippetArray', - insertText: 'snippetArray:\n - ', - }, - ]); - }); - - it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'labelSnippetArray', - insertText: ` - - item1: value - item2: value2`, - }, - ]); - }); - - it('should suggest defaultSnippet for ARRAY property - value with indent (without hyphen)', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray: - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'labelSnippetArray', - insertText: `- item1: value - item2: value2`, - }, - ]); - }); - it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray: - - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'item1', - insertText: 'item1: ', - }, - { - label: 'labelSnippetArray', - insertText: `item1: value - item2: value2`, - }, - ]); - }); - it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray: - - item1: test - - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ - { - label: 'item1', - insertText: 'item1: ', - }, - { - label: 'labelSnippetArray', - insertText: `item1: value - item2: value2`, - }, - ]); - }); - }); - describe('defaultSnippets on the items level as an object value', () => { - const schema = getNestedSchema({ - snippetArray2: { - type: 'array', - items: { - type: 'object', - additionalProperties: true, - defaultSnippets: [ - { - label: 'labelSnippetArray', - body: { - item1: 'value', - item2: 'value2', - }, - }, - ], - }, - }, - }); - - it('should suggest defaultSnippet for ARRAY property - unfinished property', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetAr|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ - 'snippetArray2:\n - item1: value\n item2: value2', - ]); - }); - - it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray2: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ - ` - - item1: value - item2: value2`, - ]); - }); - - it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray2: - - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ - `item1: value - item2: value2`, - ]); - }); - it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArray2: - - item1: test - - |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ - `item1: value - item2: value2`, - ]); - }); - }); // ARRAY - Snippet on items level - - describe('defaultSnippets on the items level, ARRAY - Body is array of primitives', () => { - const schema = getNestedSchema({ - snippetArrayPrimitives: { - type: 'array', - items: { - type: ['string', 'boolean', 'number', 'null'], - defaultSnippets: [ - { - body: ['value', 5, null, false], - }, - ], - }, - }, - }); - - // fix if needed - // it('should suggest defaultSnippet for ARRAY property with primitives - unfinished property', async () => { - // schemaProvider.addSchema(SCHEMA_ID, schema); - // const content = ` - // snippets: - // snippetArrayPrimitives|\n| - // `; - // const completion = await parseCaret(content); - - // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ - // 'snippetArrayPrimitives:\n - value\n - 5\n - null\n - false', - // ]); - // }); - - it('should suggest defaultSnippet for ARRAY property with primitives - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArrayPrimitives: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value\n - 5\n - null\n - false']); - }); - - // skip, fix if needed - // it('should suggest defaultSnippet for ARRAY property with primitives - value with indent (with hyphen)', async () => { - // schemaProvider.addSchema(SCHEMA_ID, schema); - // const content = ` - // snippets: - // snippetArrayPrimitives: - // - |\n| - // `; - // const completion = await parseCaret(content); - - // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n - 5\n - null\n - false']); - // }); - // it('should suggest defaultSnippet for ARRAY property with primitives - value on 2nd position', async () => { - // schemaProvider.addSchema(SCHEMA_ID, schema); - // const content = ` - // snippets: - // snippetArrayPrimitives: - // - some other value - // - |\n| - // `; - // const completion = await parseCaret(content); - - // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['- value\n - 5\n - null\n - false']); - // }); - }); // ARRAY - Body is array of primitives - - describe('defaultSnippets on the items level, ARRAY - Body is string', () => { - const schema = getNestedSchema({ - snippetArrayString: { - type: 'array', - items: { - type: 'string', - defaultSnippets: [ - { - body: 'value', - }, - ], - }, - }, - }); - - it('should suggest defaultSnippet for ARRAY property with string - unfinished property', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArrayString|\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - ${1}']); - // better to suggest, fix if needed - // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - value']); - }); - - it('should suggest defaultSnippet for ARRAY property with string - value after colon', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` -snippets: - snippetArrayString: |\n| -`; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value']); - }); - - it('should suggest defaultSnippet for ARRAY property with string - value with indent (with hyphen)', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` - snippets: - snippetArrayString: - - |\n| - `; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); - }); - it('should suggest defaultSnippet for ARRAY property with string - value on 2nd position', async () => { - schemaProvider.addSchema(SCHEMA_ID, schema); - const content = ` - snippets: - snippetArrayString: - - some other value - - |\n| - `; - const completion = await parseCaret(content); - - expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); - }); - }); // ARRAY - Body is simple string - }); // ARRAY - }); - describe('should suggest prop of the object (based on not completed prop name)', () => { const schema: JSONSchema = { definitions: { diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index b38a4dae3..1e41a6d72 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -2,31 +2,74 @@ * Copyright (c) Red Hat. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toFsPath, setupSchemaIDTextDocument, setupLanguageService, caretPosition } from './utils/testHelper'; import * as assert from 'assert'; +import { expect } from 'chai'; import * as path from 'path'; -import { ServiceSetup } from './utils/serviceSetup'; +import { JSONSchema } from 'vscode-json-languageservice'; +import { CompletionList, TextEdit } from 'vscode-languageserver-types'; import { LanguageHandlers } from '../src/languageserver/handlers/languageHandlers'; +import { LanguageService } from '../src/languageservice/yamlLanguageService'; import { SettingsState, TextDocumentTestManager } from '../src/yamlSettings'; -import { CompletionList, TextEdit } from 'vscode-languageserver-types'; -import { expect } from 'chai'; +import { ServiceSetup } from './utils/serviceSetup'; +import { + caretPosition, + SCHEMA_ID, + setupLanguageService, + setupSchemaIDTextDocument, + TestCustomSchemaProvider, + toFsPath, +} from './utils/testHelper'; describe('Default Snippet Tests', () => { + let languageSettingsSetup: ServiceSetup; + let languageService: LanguageService; let languageHandler: LanguageHandlers; let yamlSettings: SettingsState; + let schemaProvider: TestCustomSchemaProvider; before(() => { const uri = toFsPath(path.join(__dirname, './fixtures/defaultSnippets.json')); const fileMatch = ['*.yml', '*.yaml']; - const languageSettingsSetup = new ServiceSetup().withCompletion().withSchemaFileMatch({ + languageSettingsSetup = new ServiceSetup().withCompletion().withSchemaFileMatch({ fileMatch, uri, }); - const { languageHandler: langHandler, yamlSettings: settings } = setupLanguageService(languageSettingsSetup.languageSettings); + const { + languageService: langService, + languageHandler: langHandler, + yamlSettings: settings, + schemaProvider: testSchemaProvider, + } = setupLanguageService(languageSettingsSetup.languageSettings); + languageService = langService; languageHandler = langHandler; yamlSettings = settings; + schemaProvider = testSchemaProvider; }); + afterEach(() => { + schemaProvider.deleteSchema(SCHEMA_ID); + languageService.configure(languageSettingsSetup.languageSettings); + }); + + /** + * Generates a completion list for the given document and caret (cursor) position. + * @param content The content of the document. + * The caret is located in the content using `|` bookends. + * For example, `content = 'ab|c|d'` places the caret over the `'c'`, at `position = 2` + * @returns A list of valid completions. + */ + function parseCaret(content: string): Promise { + const { position, content: content2 } = caretPosition(content); + + const testTextDocument = setupSchemaIDTextDocument(content2); + yamlSettings.documents = new TextDocumentTestManager(); + (yamlSettings.documents as TextDocumentTestManager).set(testTextDocument); + return languageHandler.completionHandler({ + position: testTextDocument.positionAt(position), + textDocument: testTextDocument, + }); + } + describe('Snippet Tests', function () { /** * Generates a completion list for the given document and caret (cursor) position. @@ -440,4 +483,648 @@ describe('Default Snippet Tests', () => { expect(item.textEdit.newText).to.be.equal('name: some'); }); }); + + describe('variations of defaultSnippets', () => { + const getNestedSchema = (schema: JSONSchema['properties']): JSONSchema => { + return { + type: 'object', + properties: { + snippets: { + type: 'object', + properties: { + ...schema, + }, + }, + }, + }; + }; + + // STRING + describe('defaultSnippet for string property', () => { + const schema = getNestedSchema({ + snippetString: { + type: 'string', + defaultSnippets: [ + { + label: 'labelSnippetString', + body: 'value', + }, + ], + }, + }); + + it('should suggest defaultSnippet for STRING property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetStr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetString: value']); + }); + + it('should suggest defaultSnippet for STRING property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // STRING + + // OBJECT + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + required: ['item1'], + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1: 'value', + item2: { + item3: 'value nested', + }, + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value + item2: + item3: value nested`, + }, + ]); + }); + it('should suggest defaultSnippet for OBJECT property - unfinished property, should keep all snippet properties', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + item1: value + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: value + item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: ` + item1: value + item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest defaultSnippet for OBJECT property - value with indent', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', // snippet intellisense + insertText: `item1: value +item2: + item3: value nested`, + }, + { + label: 'item1', // key intellisense + insertText: 'item1: ', + }, + { + label: 'object', // parent intellisense + insertText: 'item1: ', + }, + ]); + }); + + it('should suggest partial defaultSnippet for OBJECT property - subset of items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetObject', + insertText: `item2: + item3: value nested`, + }, + ]); + }); + + it('should suggest no defaultSnippet for OBJECT property - all items already there', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetObject: + item1: val + item2: val + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([]); + }); + }); // OBJECT + + // OBJECT - Snippet nested + describe('defaultSnippet for OBJECT property', () => { + const schema = getNestedSchema({ + snippetObject: { + type: 'object', + properties: { + item1: { + type: 'object', + defaultSnippets: [ + { + label: 'labelSnippetObject', + body: { + item1_1: 'value', + item1_2: { + item1_2_1: 'value nested', + }, + }, + }, + ], + }, + }, + required: ['item1'], + }, + }); + + it('should suggest defaultSnippet for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetOb|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetObject', + insertText: `snippetObject: + item1: + item1_1: value + item1_2: + item1_2_1: value nested`, + }, + ]); + }); + }); // OBJECT - Snippet nested + + // ARRAY + describe('defaultSnippet for ARRAY property', () => { + describe('defaultSnippets on the property level as an object value', () => { + const schema = getNestedSchema({ + snippetArray: { + type: 'array', + $comment: + 'property - Not implemented, OK value, OK value nested, OK value nested with -, OK on 2nd index without or with -', + items: { + type: 'object', + properties: { + item1: { type: 'string' }, + }, + }, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property (not implemented)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'snippetArray', + insertText: 'snippetArray:\n - ', + }, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: ` + - item1: value + item2: value2`, + }, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (without hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: `- item1: value + item2: value2`, + }, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'item1', + insertText: 'item1: ', + }, + { + label: 'labelSnippetArray', + insertText: `item1: value + item2: value2`, + }, + ]); + }); + }); + describe('defaultSnippets on the items level as an object value', () => { + const schema = getNestedSchema({ + snippetArray2: { + type: 'array', + items: { + type: 'object', + additionalProperties: true, + defaultSnippets: [ + { + label: 'labelSnippetArray', + body: { + item1: 'value', + item2: 'value2', + }, + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAr|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetArray2:\n - item1: value\n item2: value2', + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + ` + - item1: value + item2: value2`, + ]); + }); + + it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - item1: test + - |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + `item1: value + item2: value2`, + ]); + }); + }); // ARRAY - Snippet on items level + + describe('defaultSnippets on the items level, ARRAY - Body is array of primitives', () => { + const schema = getNestedSchema({ + snippetArrayPrimitives: { + type: 'array', + items: { + type: ['string', 'boolean', 'number', 'null'], + defaultSnippets: [ + { + body: ['value', 5, null, false], + }, + ], + }, + }, + }); + + // fix if needed + // it('should suggest defaultSnippet for ARRAY property with primitives - unfinished property', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArrayPrimitives|\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + // 'snippetArrayPrimitives:\n - value\n - 5\n - null\n - false', + // ]); + // }); + + it('should suggest defaultSnippet for ARRAY property with primitives - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayPrimitives: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value\n - 5\n - null\n - false']); + }); + + it('should suggest defaultSnippet for ARRAY property with primitives - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayPrimitives: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n- 5\n- null\n- false']); + }); + it('should suggest defaultSnippet for ARRAY property with primitives - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayPrimitives: + - some other value + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n- 5\n- null\n- false']); + }); + }); // ARRAY - Body is array of primitives + + describe('defaultSnippets on the items level, ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetArrayObjects: { + type: 'array', + items: { + type: 'object', + defaultSnippets: [ + { + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property with objects - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects|\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetArrayObjects:\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayObjects: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + }); // ARRAY - Body is array of objects + + describe('defaultSnippets on the items level, ARRAY - Body is string', () => { + const schema = getNestedSchema({ + snippetArrayString: { + type: 'array', + items: { + type: 'string', + defaultSnippets: [ + { + body: 'value', + }, + ], + }, + }, + }); + + it('should suggest defaultSnippet for ARRAY property with string - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString|\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - ${1}']); + // better to suggest, fix if needed + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArrayString: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value']); + }); + + it('should suggest defaultSnippet for ARRAY property with string - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + it('should suggest defaultSnippet for ARRAY property with string - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - some other value + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); + }); + }); // ARRAY - Body is simple string + }); // ARRAY + }); // variations of defaultSnippets }); From 9391764e42486415c6f6f03e60bf6b156ab1982e Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Wed, 2 Apr 2025 10:38:03 +0200 Subject: [PATCH 12/14] fix: anyOfSnippets --- .../services/yamlCompletion.ts | 9 +- test/defaultSnippets.test.ts | 244 +++++++++++++++--- 2 files changed, 212 insertions(+), 41 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 3b98841e5..30530cd31 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1552,20 +1552,21 @@ export class YamlCompletion { continue; } - if ((arrayDepth === 0 && type === 'array') || isArray) { + // postprocess of the array snippet that needs special handling based on the position in the yaml + if ((arrayDepth === 0 && type === 'array') || isArray || Array.isArray(value)) { // add extra hyphen if we are in array, but the hyphen is missing on current line // but don't add it for array value because it's already there from getInsertTextForSnippetValue const addHyphen = !collector.context.hasHyphen && !Array.isArray(value) ? '- ' : ''; // add new line if the cursor is after the colon const addNewLine = collector.context.hasColon ? `\n${this.indentation}` : ''; + const addIndent = isArray || collector.context.hasColon || addHyphen ? this.indentation : ''; // add extra indent if new line and hyphen are added - const addIndent = isArray && addNewLine && addHyphen ? this.indentation : ''; - // const addIndent = addHyphen && addNewLine ? this.indentation : ''; + const addExtraIndent = isArray && addNewLine && addHyphen ? this.indentation : ''; insertText = addIndentationToMultilineString( insertText.trimStart(), `${addNewLine}${addHyphen}`, - `${addIndent}${this.indentation}` + `${addExtraIndent}${addIndent}` ); } diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 1e41a6d72..687cefe04 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -537,7 +537,7 @@ snippets: }); // STRING // OBJECT - describe('defaultSnippet for OBJECT property', () => { + describe('defaultSnippet(snippetObject) for OBJECT property', () => { const schema = getNestedSchema({ snippetObject: { type: 'object', @@ -559,7 +559,7 @@ snippets: }, }); - it('should suggest defaultSnippet for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { + it('should suggest defaultSnippet(snippetObject) for OBJECT property - unfinished property, snippet replaces autogenerated props', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -577,7 +577,7 @@ snippets: }, ]); }); - it('should suggest defaultSnippet for OBJECT property - unfinished property, should keep all snippet properties', async () => { + it('should suggest defaultSnippet(snippetObject) for OBJECT property - unfinished property, should keep all snippet properties', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -597,7 +597,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for OBJECT property - value after colon', async () => { + it('should suggest defaultSnippet(snippetObject) for OBJECT property - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -616,7 +616,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for OBJECT property - value with indent', async () => { + it('should suggest defaultSnippet(snippetObject) for OBJECT property - value with indent', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -643,7 +643,7 @@ item2: ]); }); - it('should suggest partial defaultSnippet for OBJECT property - subset of items already there', async () => { + it('should suggest partial defaultSnippet(snippetObject) for OBJECT property - subset of items already there', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -678,7 +678,7 @@ snippets: }); // OBJECT // OBJECT - Snippet nested - describe('defaultSnippet for OBJECT property', () => { + describe('defaultSnippet(snippetObject) for OBJECT property', () => { const schema = getNestedSchema({ snippetObject: { type: 'object', @@ -702,7 +702,7 @@ snippets: }, }); - it('should suggest defaultSnippet for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { + it('should suggest defaultSnippet(snippetObject) for nested OBJECT property - unfinished property, snippet extends autogenerated props', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -725,12 +725,10 @@ snippets: // ARRAY describe('defaultSnippet for ARRAY property', () => { - describe('defaultSnippets on the property level as an object value', () => { + describe('defaultSnippets(snippetArray) on the property level as an object value', () => { const schema = getNestedSchema({ snippetArray: { type: 'array', - $comment: - 'property - Not implemented, OK value, OK value nested, OK value nested with -, OK on 2nd index without or with -', items: { type: 'object', properties: { @@ -749,7 +747,7 @@ snippets: }, }); - it('should suggest defaultSnippet for ARRAY property - unfinished property (not implemented)', async () => { + it('should suggest defaultSnippet(snippetArray) for ARRAY property - unfinished property (not implemented)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -765,7 +763,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + it('should suggest defaultSnippet(snippetArray) for ARRAY property - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -783,7 +781,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property - value with indent (without hyphen)', async () => { + it('should suggest defaultSnippet(snippetArray) for ARRAY property - value with indent (without hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -800,7 +798,7 @@ snippets: }, ]); }); - it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + it('should suggest defaultSnippet(snippetArray) for ARRAY property - value with indent (with hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -821,7 +819,7 @@ snippets: }, ]); }); - it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + it('should suggest defaultSnippet(snippetArray) for ARRAY property - value on 2nd position', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -844,7 +842,7 @@ snippets: ]); }); }); - describe('defaultSnippets on the items level as an object value', () => { + describe('defaultSnippets(snippetArray2) on the items level as an object value', () => { const schema = getNestedSchema({ snippetArray2: { type: 'array', @@ -864,7 +862,7 @@ snippets: }, }); - it('should suggest defaultSnippet for ARRAY property - unfinished property', async () => { + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - unfinished property', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -877,7 +875,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property - value after colon', async () => { + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -892,7 +890,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property - value with indent (with hyphen)', async () => { + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - value with indent (with hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -906,7 +904,7 @@ snippets: item2: value2`, ]); }); - it('should suggest defaultSnippet for ARRAY property - value on 2nd position', async () => { + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - value on 2nd position', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -923,7 +921,7 @@ snippets: }); }); // ARRAY - Snippet on items level - describe('defaultSnippets on the items level, ARRAY - Body is array of primitives', () => { + describe('defaultSnippets(snippetArrayPrimitives) on the items level, ARRAY - Body is array of primitives', () => { const schema = getNestedSchema({ snippetArrayPrimitives: { type: 'array', @@ -938,8 +936,11 @@ snippets: }, }); - // fix if needed - // it('should suggest defaultSnippet for ARRAY property with primitives - unfinished property', async () => { + // implement if needed + // schema type array doesn't use defaultSnippets as a replacement for the auto generated result + // to change this, just return snippet result in `getInsertTextForProperty` function + + // it('should suggest defaultSnippet(snippetArrayPrimitives) for ARRAY property with primitives - unfinished property', async () => { // schemaProvider.addSchema(SCHEMA_ID, schema); // const content = ` // snippets: @@ -952,7 +953,7 @@ snippets: // ]); // }); - it('should suggest defaultSnippet for ARRAY property with primitives - value after colon', async () => { + it('should suggest defaultSnippet(snippetArrayPrimitives) for ARRAY property with primitives - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -963,7 +964,7 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value\n - 5\n - null\n - false']); }); - it('should suggest defaultSnippet for ARRAY property with primitives - value with indent (with hyphen)', async () => { + it('should suggest defaultSnippet(snippetArrayPrimitives) for ARRAY property with primitives - value with indent (with hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -974,7 +975,7 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value\n- 5\n- null\n- false']); }); - it('should suggest defaultSnippet for ARRAY property with primitives - value on 2nd position', async () => { + it('should suggest defaultSnippet(snippetArrayPrimitives) for ARRAY property with primitives - value on 2nd position', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -988,7 +989,94 @@ snippets: }); }); // ARRAY - Body is array of primitives - describe('defaultSnippets on the items level, ARRAY - Body is array of objects', () => { + describe('defaultSnippets(snippetArray2Objects) outside items level, ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetArray2Objects: { + type: 'array', + items: { + type: 'object', + }, + defaultSnippets: [ + { + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }); + + // schema type array doesn't use defaultSnippets as a replacement for the auto generated result + // to change this, just return snippet result in `getInsertTextForProperty` function + // it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - unfinished property', async () => { + // schemaProvider.addSchema(SCHEMA_ID, schema); + // const content = ` + // snippets: + // snippetArray2Objects|\n| + // `; + // const completion = await parseCaret(content); + + // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + // 'snippetArray2Objects:\n - item1: value\n item2: value2\n - item3: value', + // ]); + // }); + + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2Objects: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value with indent (without hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['- item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + }); // ARRAY outside items - Body is array of objects + + describe('defaultSnippets(snippetArrayObjects) on the items level, ARRAY - Body is array of objects', () => { const schema = getNestedSchema({ snippetArrayObjects: { type: 'array', @@ -1011,7 +1099,7 @@ snippets: }, }); - it('should suggest defaultSnippet for ARRAY property with objects - unfinished property', async () => { + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - unfinished property', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1024,7 +1112,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property with objects - value after colon', async () => { + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1037,7 +1125,7 @@ snippets: ]); }); - it('should suggest defaultSnippet for ARRAY property with objects - value with indent (with hyphen)', async () => { + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value with indent (with hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1048,7 +1136,7 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); }); - it('should suggest defaultSnippet for ARRAY property with objects - value on 2nd position', async () => { + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value on 2nd position', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1062,7 +1150,7 @@ snippets: }); }); // ARRAY - Body is array of objects - describe('defaultSnippets on the items level, ARRAY - Body is string', () => { + describe('defaultSnippets(snippetArrayString) on the items level, ARRAY - Body is string', () => { const schema = getNestedSchema({ snippetArrayString: { type: 'array', @@ -1077,7 +1165,7 @@ snippets: }, }); - it('should suggest defaultSnippet for ARRAY property with string - unfinished property', async () => { + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - unfinished property', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1090,7 +1178,7 @@ snippets: // expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['snippetArrayString:\n - value']); }); - it('should suggest defaultSnippet for ARRAY property with string - value after colon', async () => { + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - value after colon', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1101,7 +1189,7 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['\n - value']); }); - it('should suggest defaultSnippet for ARRAY property with string - value with indent (with hyphen)', async () => { + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - value with indent (with hyphen)', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1112,7 +1200,7 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); }); - it('should suggest defaultSnippet for ARRAY property with string - value on 2nd position', async () => { + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - value on 2nd position', async () => { schemaProvider.addSchema(SCHEMA_ID, schema); const content = ` snippets: @@ -1126,5 +1214,87 @@ snippets: }); }); // ARRAY - Body is simple string }); // ARRAY + + describe('anyOf(snippetAnyOfArray), ARRAY - Body is array of objects', () => { + const schema = getNestedSchema({ + snippetAnyOfArray: { + anyOf: [ + { + items: { + type: 'object', + }, + }, + { + type: 'object', + }, + ], + + defaultSnippets: [ + { + label: 'labelSnippetAnyOfArray', + body: [ + { + item1: 'value', + item2: 'value2', + }, + { + item3: 'value', + }, + ], + }, + ], + }, + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - unfinished property', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray|\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + 'snippetAnyOfArray:\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value after colon', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetAnyOfArray: |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '\n - item1: value\n item2: value2\n - item3: value', + ]); + }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value with indent (with hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - 1st: 1 + - |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); + }); + }); // anyOf - Body is array of objects }); // variations of defaultSnippets }); From a5508d76a018e73fa906e18a31b16f1041e8e3f6 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Wed, 2 Apr 2025 14:04:33 +0200 Subject: [PATCH 13/14] fix: exclude existing props in array snippet --- src/languageservice/utils/json.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languageservice/utils/json.ts b/src/languageservice/utils/json.ts index 43a1be2dd..cc0deebf0 100644 --- a/src/languageservice/utils/json.ts +++ b/src/languageservice/utils/json.ts @@ -67,7 +67,7 @@ export function stringifyObject( for (let i = 0; i < keys.length; i++) { const key = keys[i]; - if (depth === 0 && settings.existingProps.includes(key)) { + if (depth === 0 && settings.existingProps.includes(key.replace(/^[-\s]+/, ''))) { // Don't add existing properties to the YAML continue; } From c1c1d54ae93e5e51e725e2c65af81c17435321aa Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Thu, 3 Apr 2025 13:00:46 +0200 Subject: [PATCH 14/14] fix: array snippet for 2nd position without hyphen --- .../services/yamlCompletion.ts | 2 +- test/defaultSnippets.test.ts | 120 ++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 30530cd31..65608d5eb 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -1559,7 +1559,7 @@ export class YamlCompletion { const addHyphen = !collector.context.hasHyphen && !Array.isArray(value) ? '- ' : ''; // add new line if the cursor is after the colon const addNewLine = collector.context.hasColon ? `\n${this.indentation}` : ''; - const addIndent = isArray || collector.context.hasColon || addHyphen ? this.indentation : ''; + const addIndent = collector.context.hasColon || addHyphen ? this.indentation : ''; // add extra indent if new line and hyphen are added const addExtraIndent = isArray && addNewLine && addHyphen ? this.indentation : ''; diff --git a/test/defaultSnippets.test.ts b/test/defaultSnippets.test.ts index 687cefe04..3fc8c53f1 100644 --- a/test/defaultSnippets.test.ts +++ b/test/defaultSnippets.test.ts @@ -841,6 +841,29 @@ snippets: }, ]); }); + + it('should suggest defaultSnippet(snippetArray) for ARRAY property - value on 2nd position', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray: + - item1: test + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map(({ label, insertText }) => ({ label, insertText }))).to.be.deep.equal([ + { + label: 'labelSnippetArray', + insertText: `- item1: value + item2: value2`, + }, + { + label: '- (array item) object', + insertText: '- ', + }, + ]); + }); }); describe('defaultSnippets(snippetArray2) on the items level as an object value', () => { const schema = getNestedSchema({ @@ -919,6 +942,27 @@ snippets: item2: value2`, ]); }); + it('should suggest defaultSnippet(snippetArray2) for ARRAY property - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` +snippets: + snippetArray2: + - item1: test + |\n| +`; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- item1: value\n item2: value2', + label: 'labelSnippetArray', + }, + { + insertText: '- item1: value\n item2: value2', + label: '- (array item) object', + }, + ]); + }); }); // ARRAY - Snippet on items level describe('defaultSnippets(snippetArrayPrimitives) on the items level, ARRAY - Body is array of primitives', () => { @@ -998,6 +1042,7 @@ snippets: }, defaultSnippets: [ { + label: 'snippetArray2Objects', body: [ { item1: 'value', @@ -1074,6 +1119,27 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); }); + it('should suggest defaultSnippet(snippetArray2Objects) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArray2Objects: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- item1: value\n item2: value2\n- item3: value', + label: 'snippetArray2Objects', + }, + { + insertText: '- $1\n', + label: '- (array item) object', + }, + ]); + }); }); // ARRAY outside items - Body is array of objects describe('defaultSnippets(snippetArrayObjects) on the items level, ARRAY - Body is array of objects', () => { @@ -1148,6 +1214,21 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); }); + it('should suggest defaultSnippet(snippetArrayObjects) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayObjects: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => i.insertText)).to.be.deep.equal([ + '- item1: value\n item2: value2\n- item3: value', // array item snippet from getInsertTextForObject + '- item1: value\n item2: value2\n- item3: value', // from collectDefaultSnippets + ]); + }); }); // ARRAY - Body is array of objects describe('defaultSnippets(snippetArrayString) on the items level, ARRAY - Body is string', () => { @@ -1212,6 +1293,27 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['value']); }); + it('should suggest defaultSnippet(snippetArrayString) for ARRAY property with string - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetArrayString: + - some other value + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- value', + label: '"value"', + }, + { + insertText: '- value', + label: '- (array item) string', + }, + ]); + }); }); // ARRAY - Body is simple string }); // ARRAY @@ -1295,6 +1397,24 @@ snippets: expect(completion.items.map((i) => i.insertText)).to.be.deep.equal(['item1: value\n item2: value2\n- item3: value']); }); + + it('should suggest defaultSnippet(snippetAnyOfArray) for ARRAY property with objects - value on 2nd position (no hyphen)', async () => { + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = ` + snippets: + snippetAnyOfArray: + - 1st: 1 + |\n| + `; + const completion = await parseCaret(content); + + expect(completion.items.map((i) => ({ label: i.label, insertText: i.insertText }))).to.be.deep.equal([ + { + insertText: '- $1\n', // could be better to suggest snippet - todo + label: '- (array item) object', + }, + ]); + }); }); // anyOf - Body is array of objects }); // variations of defaultSnippets });