From f61734ccc9fa964b2d2efe596a7c63b214d11c7f Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Wed, 17 May 2023 10:23:22 +0200 Subject: [PATCH 1/4] fix: array item with existing property --- .../services/yamlCompletion.ts | 36 ++++++----- test/autoCompletionFix.test.ts | 62 +++++++++++++++++++ 2 files changed, 83 insertions(+), 15 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 99e061f0b..b2efbd680 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -144,11 +144,11 @@ export class YamlCompletion { this.arrayPrefixIndentation = ''; let overwriteRange: Range = null; + const isOnlyHyphen = lineContent.match(/^\s*(-)\s*$/); if (areOnlySpacesAfterPosition) { overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); const isOnlyWhitespace = lineContent.trim().length === 0; - const isOnlyDash = lineContent.match(/^\s*(-)\s*$/); - if (node && isScalar(node) && !isOnlyWhitespace && !isOnlyDash) { + if (node && isScalar(node) && !isOnlyWhitespace && !isOnlyHyphen) { const lineToPosition = lineContent.substring(0, position.character); const matches = // get indentation of unfinished property (between indent and cursor) @@ -357,6 +357,12 @@ export class YamlCompletion { if (node) { if (lineContent.length === 0) { node = currentDoc.internalDocument.contents as Node; + } else if (isSeq(node) && isOnlyHyphen) { + const index = this.findItemAtOffset(node, document, offset); + const item = node.items[index]; + if (isNode(item)) { + node = item; + } } else { const parent = currentDoc.getParent(node); if (parent) { @@ -717,19 +723,19 @@ export class YamlCompletion { const propertySchema = schemaProperties[key]; if (typeof propertySchema === 'object' && !propertySchema.deprecationMessage && !propertySchema['doNotSuggest']) { - let identCompensation = ''; + let indentCompensation = ''; if (nodeParent && isSeq(nodeParent) && node.items.length <= 1 && !hasOnlyWhitespace) { - // because there is a slash '-' to prevent the properties generated to have the correct - // indent - const sourceText = textBuffer.getText(); - const indexOfSlash = sourceText.lastIndexOf('-', node.range[0] - 1); - if (indexOfSlash >= 0) { - // add one space to compensate the '-' - const overwriteChars = overwriteRange.end.character - overwriteRange.start.character; - identCompensation = ' ' + sourceText.slice(indexOfSlash + 1, node.range[1] - overwriteChars); + // because there is a slash '-' to prevent the properties generated to have the correct indent + const fromLastHyphenToPosition = lineContent.slice( + lineContent.lastIndexOf('-'), + overwriteRange.start.character + ); + const hyphenFollowedByEmpty = fromLastHyphenToPosition.match(/-([ \t]*)/); + if (hyphenFollowedByEmpty) { + indentCompensation = ' ' + hyphenFollowedByEmpty[1]; } } - identCompensation += this.arrayPrefixIndentation; + indentCompensation += this.arrayPrefixIndentation; // if check that current node has last pair with "null" value and key witch match key from schema, // and if schema has array definition it add completion item for array item creation @@ -760,7 +766,7 @@ export class YamlCompletion { key, propertySchema, separatorAfter, - identCompensation + this.indentation + indentCompensation + this.indentation ); } const isNodeNull = @@ -787,13 +793,13 @@ export class YamlCompletion { key, propertySchema, separatorAfter, - identCompensation + this.indentation + indentCompensation + this.indentation ), insertTextFormat: InsertTextFormat.Snippet, documentation: this.fromMarkup(propertySchema.markdownDescription) || propertySchema.description || '', parent: { schema: schema.schema, - indent: identCompensation, + indent: indentCompensation, }, }); } diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 81053315b..a88b9787c 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -865,6 +865,30 @@ objB: }) ); }); + it('indent compensation for partial key with trailing spaces', async () => { + const schema: JSONSchema = { + type: 'object', + properties: { + array: { + type: 'array', + items: { + type: 'object', + properties: { + obj1: { + type: 'object', + }, + }, + }, + }, + }, + }; + languageService.addSchema(SCHEMA_ID, schema); + const content = 'array:\n - na '; + const completion = await parseSetup(content, 1, 5); + + expect(completion.items.length).equal(1); + expect(completion.items[0].insertText).eql('obj1:\n '); + }); describe('partial value with trailing spaces', () => { it('partial value with trailing spaces', async () => { @@ -1161,6 +1185,44 @@ objB: expect(result.items.length).to.be.equal(1); expect(result.items[0].insertText).to.be.equal('objA:\n itemA: '); }); + + describe('array item with existing property', () => { + const schema: JSONSchema = { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'object', + properties: { + objA: { + type: 'object', + }, + propB: { + const: 'test', + }, + }, + }, + }, + }, + }; + it('should get extra space compensation for the 1st prop in array object item', async () => { + languageService.addSchema(SCHEMA_ID, schema); + const content = 'array1:\n - \n propB: test'; + const result = await parseSetup(content, 1, 4); // after `- ` + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].insertText).to.be.equal('objA:\n '); + }); + it('should get extra space compensation for the 1st prop in array object item - extra spaces', async () => { + languageService.addSchema(SCHEMA_ID, schema); + const content = 'array1:\n - \n propB: test'; + const result = await parseSetup(content, 1, 4); // after `- ` + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].insertText).to.be.equal('objA:\n '); + }); + }); }); //'extra space after cursor' it('should suggest from additionalProperties', async () => { From be8ace585a8bc30a65c545e829842d9f5fbb0ff3 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Tue, 6 Jun 2023 11:13:29 +0200 Subject: [PATCH 2/4] fix: some PR comments --- .../services/yamlCompletion.ts | 2 +- test/autoCompletionFix.test.ts | 21 +++-- test/yaml-documents.test.ts | 77 +++++++++++++++++-- 3 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index b2efbd680..0e73a54d5 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -144,7 +144,7 @@ export class YamlCompletion { this.arrayPrefixIndentation = ''; let overwriteRange: Range = null; - const isOnlyHyphen = lineContent.match(/^\s*(-)\s*$/); + const isOnlyHyphen = lineContent.match(/^\s*(-)\s*($|#)/); if (areOnlySpacesAfterPosition) { overwriteRange = Range.create(position, Position.create(position.line, lineContent.length)); const isOnlyWhitespace = lineContent.trim().length === 0; diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index a88b9787c..2486246ad 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -883,8 +883,8 @@ objB: }, }; languageService.addSchema(SCHEMA_ID, schema); - const content = 'array:\n - na '; - const completion = await parseSetup(content, 1, 5); + const content = 'array:\n - na| | '; + const completion = await parseCaret(content); expect(completion.items.length).equal(1); expect(completion.items[0].insertText).eql('obj1:\n '); @@ -1208,16 +1208,25 @@ objB: }; it('should get extra space compensation for the 1st prop in array object item', async () => { languageService.addSchema(SCHEMA_ID, schema); - const content = 'array1:\n - \n propB: test'; - const result = await parseSetup(content, 1, 4); // after `- ` + const content = 'array1:\n - |\n| propB: test'; + const result = await parseCaret(content); expect(result.items.length).to.be.equal(1); expect(result.items[0].insertText).to.be.equal('objA:\n '); }); it('should get extra space compensation for the 1st prop in array object item - extra spaces', async () => { languageService.addSchema(SCHEMA_ID, schema); - const content = 'array1:\n - \n propB: test'; - const result = await parseSetup(content, 1, 4); // after `- ` + const content = 'array1:\n - | | \n propB: test'; + const result = await parseCaret(content); + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].insertText).to.be.equal('objA:\n '); + }); + // previous PR doesn't fix this + it.skip('should get extra space compensation for the 1st prop in array object item - extra lines', async () => { + languageService.addSchema(SCHEMA_ID, schema); + const content = 'array1:\n - \n |\n| propB: test'; + const result = await parseCaret(content); expect(result.items.length).to.be.equal(1); expect(result.items[0].insertText).to.be.equal('objA:\n '); diff --git a/test/yaml-documents.test.ts b/test/yaml-documents.test.ts index c2278b969..d4c08618f 100644 --- a/test/yaml-documents.test.ts +++ b/test/yaml-documents.test.ts @@ -211,16 +211,77 @@ objB: expect(((result as YAMLMap).items[0].key as Scalar).value).eqls('bar'); }); - it('Find closes node: array', () => { - const doc = setupTextDocument('foo:\n - bar: aaa\n '); - const yamlDoc = documents.getYamlDocument(doc); - const textBuffer = new TextBuffer(doc); + describe('Array', () => { + it('Find closes node: array', () => { + const doc = setupTextDocument('foo:\n - bar: aaa\n '); + const yamlDoc = documents.getYamlDocument(doc); + const textBuffer = new TextBuffer(doc); + + const result = yamlDoc.documents[0].findClosestNode(20, textBuffer); + + expect(result).is.not.undefined; + expect(isSeq(result)).is.true; + expect((((result as YAMLSeq).items[0] as YAMLMap).items[0].key as Scalar).value).eqls('bar'); + }); + it.skip('Find first array item node', () => { + const doc = setupTextDocument(`foo: + - + item1: aaa +`); + const yamlDoc = documents.getYamlDocument(doc); + const textBuffer = new TextBuffer(doc); + + const result = yamlDoc.documents[0].findClosestNode(9, textBuffer); + + expect(result).is.not.undefined; + expect(isMap(result)).is.true; + expect(((result as YAMLMap).items[0].key as Scalar).value).eqls('item1'); + }); + it.skip('Find first array item node - extra indent', () => { + const doc = setupTextDocument(`foo: + - + + item1: aaa +`); + const yamlDoc = documents.getYamlDocument(doc); + const textBuffer = new TextBuffer(doc); + + const result = yamlDoc.documents[0].findClosestNode(9, textBuffer); + + expect(result).is.not.undefined; + expect(isMap(result)).is.true; + expect(((result as YAMLMap).items[0].key as Scalar).value).eqls('item1'); + }); + + it.skip('Find second array item node', () => { + const doc = setupTextDocument(`foo: + - item1: aaa + - + item2: bbb`); + const yamlDoc = documents.getYamlDocument(doc); + const textBuffer = new TextBuffer(doc); + + const result = yamlDoc.documents[0].findClosestNode(24, textBuffer); + + expect(result).is.not.undefined; + expect(isMap(result)).is.true; + expect(((result as YAMLMap).items[0].key as Scalar).value).eqls('item2'); + }); + it.skip('Find second array item node: - extra indent', () => { + const doc = setupTextDocument(`foo: + - item1: aaa + - + + item2: bbb`); + const yamlDoc = documents.getYamlDocument(doc); + const textBuffer = new TextBuffer(doc); - const result = yamlDoc.documents[0].findClosestNode(20, textBuffer); + const result = yamlDoc.documents[0].findClosestNode(28, textBuffer); - expect(result).is.not.undefined; - expect(isSeq(result)).is.true; - expect((((result as YAMLSeq).items[0] as YAMLMap).items[0].key as Scalar).value).eqls('bar'); + expect(result).is.not.undefined; + expect(isMap(result)).is.true; + expect(((result as YAMLMap).items[0].key as Scalar).value).eqls('item2'); + }); }); it('Find closes node: root map', () => { From 7ca0300bd529d383f310b6778b58b64b5e41aeb9 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Mon, 17 Jul 2023 13:00:47 +0200 Subject: [PATCH 3/4] chore: add another test and fix failing tests --- test/autoCompletionFix.test.ts | 52 +++++++++++++++++++++++++--------- test/yaml-documents.test.ts | 12 ++++++++ 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 2486246ad..535fd5e9b 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -882,8 +882,8 @@ objB: }, }, }; - languageService.addSchema(SCHEMA_ID, schema); - const content = 'array:\n - na| | '; + schemaProvider.addSchema(SCHEMA_ID, schema); + const content = 'array:\n - obj| | '; const completion = await parseCaret(content); expect(completion.items.length).equal(1); @@ -1109,6 +1109,41 @@ objB: expect(result.items.length).to.be.equal(1); expect(result.items[0].insertText).to.be.equal('objA:\n itemA: '); }); + + it('array completion - should suggest correct indent when extra spaces after cursor followed by with different array item', async () => { + schemaProvider.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: + - | | + - objA: + itemA: test`; + const result = await parseCaret(content); + + 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 () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object', @@ -1207,7 +1242,7 @@ objB: }, }; it('should get extra space compensation for the 1st prop in array object item', async () => { - languageService.addSchema(SCHEMA_ID, schema); + schemaProvider.addSchema(SCHEMA_ID, schema); const content = 'array1:\n - |\n| propB: test'; const result = await parseCaret(content); @@ -1215,19 +1250,10 @@ objB: expect(result.items[0].insertText).to.be.equal('objA:\n '); }); it('should get extra space compensation for the 1st prop in array object item - extra spaces', async () => { - languageService.addSchema(SCHEMA_ID, schema); + schemaProvider.addSchema(SCHEMA_ID, schema); const content = 'array1:\n - | | \n propB: test'; const result = await parseCaret(content); - expect(result.items.length).to.be.equal(1); - expect(result.items[0].insertText).to.be.equal('objA:\n '); - }); - // previous PR doesn't fix this - it.skip('should get extra space compensation for the 1st prop in array object item - extra lines', async () => { - languageService.addSchema(SCHEMA_ID, schema); - const content = 'array1:\n - \n |\n| propB: test'; - const result = await parseCaret(content); - expect(result.items.length).to.be.equal(1); expect(result.items[0].insertText).to.be.equal('objA:\n '); }); diff --git a/test/yaml-documents.test.ts b/test/yaml-documents.test.ts index d4c08618f..a544ce18e 100644 --- a/test/yaml-documents.test.ts +++ b/test/yaml-documents.test.ts @@ -212,6 +212,18 @@ objB: }); describe('Array', () => { + // Problem in `getNodeFromPosition` function. This method doesn't give proper results for arrays + // for example, this yaml return nodes: + // foo: + // - # foo object is returned (should be foo[0]) + // # foo object is returned (should be foo[0]) + // item1: aaaf + // # foo[0] object is returned (OK) + // - # foo object is returned (should be foo[1]) + // # foo[!!0!!] object is returned (should be foo[1]) + // item2: bbb + // # foo[1] object is returned (OK) + it('Find closes node: array', () => { const doc = setupTextDocument('foo:\n - bar: aaa\n '); const yamlDoc = documents.getYamlDocument(doc); From 5594fea7359003464150e23a1fd621a053b12b41 Mon Sep 17 00:00:00 2001 From: Petr Spacek Date: Wed, 29 Nov 2023 13:51:34 +0100 Subject: [PATCH 4/4] fix: correct indent when cursor is just after hyphen with trailing spaces --- .../services/yamlCompletion.ts | 2 ++ test/autoCompletionFix.test.ts | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/src/languageservice/services/yamlCompletion.ts b/src/languageservice/services/yamlCompletion.ts index 0e73a54d5..b0f979723 100644 --- a/src/languageservice/services/yamlCompletion.ts +++ b/src/languageservice/services/yamlCompletion.ts @@ -162,6 +162,8 @@ export class YamlCompletion { Position.create(position.line, lineContent.length) ); } + } else if (node && isScalar(node) && node.value === null && currentWord === '-') { + this.arrayPrefixIndentation = ' '; } } else if (node && isScalar(node) && node.value === 'null') { const nodeStartPos = document.positionAt(node.range[0]); diff --git a/test/autoCompletionFix.test.ts b/test/autoCompletionFix.test.ts index 535fd5e9b..0011f7dbc 100644 --- a/test/autoCompletionFix.test.ts +++ b/test/autoCompletionFix.test.ts @@ -1144,6 +1144,42 @@ test: expect(result.items[0].insertText).to.be.equal('objA:\n itemA: '); }); + it('array completion - should suggest correct indent when cursor is just after hyphen with trailing spaces', async () => { + schemaProvider.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: + -| | +`; + const result = await parseCaret(content); + + expect(result.items.length).to.be.equal(1); + expect(result.items[0].textEdit).to.deep.equal({ + newText: ' objA:\n itemA: ', + // range should contains all the trailing spaces + range: Range.create(2, 3, 2, 9), + }); + }); it('array of arrays completion - should suggest correct indent when extra spaces after cursor', async () => { schemaProvider.addSchema(SCHEMA_ID, { type: 'object',