From d1bafdd9425467ac8daf7cdc4f651a06236ae985 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 Mar 2023 02:54:47 +0300 Subject: [PATCH 1/2] Insert missing comma or colon before completion --- src/services/jsonCompletion.ts | 82 +++++++++++++++++++++++++++++-- src/test/completion.test.ts | 88 ++++++++++++++++++++++++++++++++-- 2 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/services/jsonCompletion.ts b/src/services/jsonCompletion.ts index 6234ed9f..2d4c2bc5 100644 --- a/src/services/jsonCompletion.ts +++ b/src/services/jsonCompletion.ts @@ -15,7 +15,7 @@ import { PromiseConstructor, Thenable, ASTNode, ObjectASTNode, ArrayASTNode, PropertyASTNode, ClientCapabilities, TextDocument, - CompletionItem, CompletionItemKind, CompletionList, Position, Range, TextEdit, InsertTextFormat, MarkupContent, MarkupKind + CompletionItem, CompletionItemKind, CompletionList, Position, Range, TextEdit, InsertTextFormat, MarkupContent, MarkupKind, StringASTNode } from '../jsonLanguageTypes'; import * as l10n from '@vscode/l10n'; @@ -86,6 +86,27 @@ export class JSONCompletion { const supportsCommitCharacters = false; //this.doesSupportsCommitCharacters(); disabled for now, waiting for new API: https://github.com/microsoft/vscode/issues/42544 const proposed = new Map(); + let insertPrevEdit: { nodeAfter: ASTNode, char: string } | undefined; + const currentPropKey = node?.type === 'property' ? node.keyNode : undefined; + if (currentPropKey && offset > currentPropKey.offset + currentPropKey.length) { + if (this.insertColonAfterProperty(document, currentPropKey)) { + insertPrevEdit = { + nodeAfter: currentPropKey, + char: ':', + }; + } + } else { + const prevNodeForComma = this.getPreviousNode(node, document, offset); + if (prevNodeForComma) { + const prevNodeEnd = prevNodeForComma.offset + prevNodeForComma.length; + if (prevNodeEnd < offset && this.evaluateSeparatorAfter(document, prevNodeEnd, offset) === ',') { + insertPrevEdit = { + nodeAfter: prevNodeForComma, + char: ',', + }; + } + } + } const collector: CompletionsCollector = { add: (suggestion: CompletionItem) => { let label = suggestion.label; @@ -105,6 +126,17 @@ export class JSONCompletion { suggestion.commitCharacters = suggestion.kind === CompletionItemKind.Property ? propertyCommitCharacters : valueCommitCharacters; } suggestion.label = label; + if (insertPrevEdit) { + const { nodeAfter } = insertPrevEdit; + const insertPrevPos = document.positionAt(nodeAfter.offset + nodeAfter.length); + suggestion.additionalTextEdits = [{ + range: { + start: insertPrevPos, + end: insertPrevPos + }, + newText: insertPrevEdit.char, + }]; + } proposed.set(label, suggestion); result.items.push(suggestion); } else { @@ -736,7 +768,7 @@ export class JSONCompletion { } private getInsertTextForPlainText(text: string): string { - return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } + return text.replace(/[\\\$\}]/g, '\\$&'); // escape $, \ and } } private getInsertTextForValue(value: any, separatorAfter: string): string { @@ -913,10 +945,14 @@ export class JSONCompletion { return text.substring(i + 1, offset); } - private evaluateSeparatorAfter(document: TextDocument, offset: number) { + private evaluateSeparatorAfter(document: TextDocument, offset: number, validateOffset?: number) { const scanner = Json.createScanner(document.getText(), true); scanner.setPosition(offset); const token = scanner.scan(); + // Insert if didn't find comma before requesting offset + if (validateOffset && scanner.getPosition() > validateOffset) { + return ','; + } switch (token) { case Json.SyntaxKind.CommaToken: case Json.SyntaxKind.CloseBraceToken: @@ -928,6 +964,13 @@ export class JSONCompletion { } } + private insertColonAfterProperty(document: TextDocument, node: StringASTNode) { + const scanner = Json.createScanner(document.getText(), true); + scanner.setPosition(node.offset + node.length); + const token = scanner.scan(); + return token !== Json.SyntaxKind.ColonToken; + } + private findItemAtOffset(node: ArrayASTNode, document: TextDocument, offset: number) { const scanner = Json.createScanner(document.getText(), true); const children = node.items; @@ -947,6 +990,39 @@ export class JSONCompletion { return 0; } + /** Find last item after offset */ + private getPreviousNode(node: ASTNode | undefined , document: TextDocument, offset: number) { + switch (node?.type) { + case 'string': + case 'number': + case 'boolean': + case 'property': + case 'null': { + node = node?.parent?.type === 'property' ? node.parent.parent : node?.parent; + } + } + if (!node) { + return; + } + const { children } = node; + if (!children) { + return; + } + let foundIndex: number | undefined; + for (let i = children.length - 1; i >= 0; i--) { + const child = children[i]; + if (offset > child.offset + child.length) { + foundIndex = i; + break; + } else if (offset >= child.offset) { + foundIndex = i - 1; + break; + } + } + const previousNode = foundIndex !== undefined ? children[foundIndex] : undefined; + return previousNode?.type === 'property' ? previousNode.valueNode : previousNode; + } + private isInComment(document: TextDocument, start: number, offset: number) { const scanner = Json.createScanner(document.getText(), false); scanner.setPosition(start); diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index 29610748..00a7f33f 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -41,7 +41,7 @@ const assertCompletion = function (completions: CompletionList, expected: ItemDe } if (expected.resultText !== undefined && match.textEdit !== undefined) { const edit = TextEdit.is(match.textEdit) ? match.textEdit : TextEdit.replace(match.textEdit.replace, match.textEdit.newText); - assert.equal(applyEdits(document, [edit]), expected.resultText); + assert.equal(applyEdits(document, [edit, ...match.additionalTextEdits ?? []]), expected.resultText); } if (expected.sortText !== undefined) { assert.equal(match.sortText, expected.sortText); @@ -198,7 +198,7 @@ suite('JSON Completion', () => { await testCompletionsFor('{ "b": 1 "a|}', schema, { count: 2, items: [ - { label: 'a', documentation: 'A', resultText: '{ "b": 1 "a": ${1:0}' } + { label: 'a', documentation: 'A', resultText: '{ "b": 1, "a": ${1:0}' } ] }); await testCompletionsFor('{ "|}', schema, { @@ -229,7 +229,7 @@ suite('JSON Completion', () => { }); await testCompletionsFor('{ "a": 1 "b|"}', schema, { items: [ - { label: 'b', documentation: 'B', resultText: '{ "a": 1 "b": "$1"}' }, + { label: 'b', documentation: 'B', resultText: '{ "a": 1, "b": "$1"}' }, ] }); await testCompletionsFor('{ "c|"\n"b": "v"}', schema, { @@ -589,6 +589,88 @@ suite('JSON Completion', () => { }); }); + test('Insert comma or colon before', async function () { + + const schema: JSONSchema = { + type: 'object', + properties: { + 'a': { + type: 'array', + items: { + type: 'boolean', + }, + }, + 'b': { + type: 'boolean', + }, + c: {} + } + }; + // isnert comma + await testCompletionsFor('{ "a": [] | }', schema, { + count: 2, + items: [ + { label: 'c', resultText: '{ "a": [], "c" }' }, + ] + }); + await testCompletionsFor('{ "a": [] "|" }', schema, { + count: 2, + items: [ + { label: 'c', resultText: '{ "a": [], "c" }' }, + ] + }); + await testCompletionsFor('{ "a": [] |"c" }', schema, { + count: 2, + items: [ + { label: 'c', resultText: '{ "a": [], "c" }' }, + ] + }); + // check only colon + await testCompletionsFor('{ "c": "" "a": | }', schema, { + count: 1, + items: [ + { label: '[]', resultText: '{ "c": "" "a": [$1] }' }, + ] + }); + + // array + await testCompletionsFor('{ "a": [ false t| ] }', schema, { + count: 2, + items: [ + { label: 'true', resultText: '{ "a": [ false, true ] }' }, + ] + }); + await testCompletionsFor('{ "a": [ false |true ] }', schema, { + count: 2, + items: [ + { label: 'true', resultText: '{ "a": [ false, true ] }' }, + ] + }); + + // insert colon + + // maybe insert comma as well? + await testCompletionsFor('{ "c": "" "a" | }', schema, { + count: 1, + items: [ + { label: '[]', resultText: '{ "c": "" "a": [$1] }' }, + ] + }); + await testCompletionsFor('{ "c": "", "a" | }', schema, { + count: 1, + items: [ + { label: '[]', resultText: '{ "c": "", "a": [$1] }' }, + ] + }); + // but it doesn't insert when in string node + await testCompletionsFor('{ "a": "", "b" t| }', schema, { + count: 2, + items: [ + { label: 'true', resultText: '{ "a": "", "b": true }' }, + ] + }); + }); + test('Complete with required anyOf', async function () { const schema: JSONSchema = { From 262e341d087df4dd1ba786e9c8177f0d81513545 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Sat, 5 Aug 2023 21:59:54 +0300 Subject: [PATCH 2/2] remove colon inserting, focus on comma only --- src/services/jsonCompletion.ts | 30 +++++++----------------------- src/test/completion.test.ts | 21 ++++++--------------- 2 files changed, 13 insertions(+), 38 deletions(-) diff --git a/src/services/jsonCompletion.ts b/src/services/jsonCompletion.ts index 2d4c2bc5..6cc2d0c2 100644 --- a/src/services/jsonCompletion.ts +++ b/src/services/jsonCompletion.ts @@ -87,26 +87,17 @@ export class JSONCompletion { const proposed = new Map(); let insertPrevEdit: { nodeAfter: ASTNode, char: string } | undefined; - const currentPropKey = node?.type === 'property' ? node.keyNode : undefined; - if (currentPropKey && offset > currentPropKey.offset + currentPropKey.length) { - if (this.insertColonAfterProperty(document, currentPropKey)) { + const prevNodeForComma = this.getPreviousNode(node, document, offset); + if (prevNodeForComma) { + const prevNodeEnd = prevNodeForComma.offset + prevNodeForComma.length; + if (prevNodeEnd < offset && this.evaluateSeparatorAfter(document, prevNodeEnd, offset) === ',') { insertPrevEdit = { - nodeAfter: currentPropKey, - char: ':', + nodeAfter: prevNodeForComma, + char: ',', }; } - } else { - const prevNodeForComma = this.getPreviousNode(node, document, offset); - if (prevNodeForComma) { - const prevNodeEnd = prevNodeForComma.offset + prevNodeForComma.length; - if (prevNodeEnd < offset && this.evaluateSeparatorAfter(document, prevNodeEnd, offset) === ',') { - insertPrevEdit = { - nodeAfter: prevNodeForComma, - char: ',', - }; - } - } } + const collector: CompletionsCollector = { add: (suggestion: CompletionItem) => { let label = suggestion.label; @@ -964,13 +955,6 @@ export class JSONCompletion { } } - private insertColonAfterProperty(document: TextDocument, node: StringASTNode) { - const scanner = Json.createScanner(document.getText(), true); - scanner.setPosition(node.offset + node.length); - const token = scanner.scan(); - return token !== Json.SyntaxKind.ColonToken; - } - private findItemAtOffset(node: ArrayASTNode, document: TextDocument, offset: number) { const scanner = Json.createScanner(document.getText(), true); const children = node.items; diff --git a/src/test/completion.test.ts b/src/test/completion.test.ts index 00a7f33f..6d0183e2 100644 --- a/src/test/completion.test.ts +++ b/src/test/completion.test.ts @@ -589,7 +589,7 @@ suite('JSON Completion', () => { }); }); - test('Insert comma or colon before', async function () { + test('Insert comma before', async function () { const schema: JSONSchema = { type: 'object', @@ -606,7 +606,7 @@ suite('JSON Completion', () => { c: {} } }; - // isnert comma + // insert comma await testCompletionsFor('{ "a": [] | }', schema, { count: 2, items: [ @@ -625,11 +625,11 @@ suite('JSON Completion', () => { { label: 'c', resultText: '{ "a": [], "c" }' }, ] }); - // check only colon + // probably only colon should be inserted await testCompletionsFor('{ "c": "" "a": | }', schema, { count: 1, items: [ - { label: '[]', resultText: '{ "c": "" "a": [$1] }' }, + { label: '[]', resultText: '{ "c": "", "a": [$1] }' }, ] }); @@ -647,26 +647,17 @@ suite('JSON Completion', () => { ] }); - // insert colon - // maybe insert comma as well? await testCompletionsFor('{ "c": "" "a" | }', schema, { count: 1, items: [ - { label: '[]', resultText: '{ "c": "" "a": [$1] }' }, + { label: '[]', resultText: '{ "c": "", "a" [$1] }' }, ] }); await testCompletionsFor('{ "c": "", "a" | }', schema, { count: 1, items: [ - { label: '[]', resultText: '{ "c": "", "a": [$1] }' }, - ] - }); - // but it doesn't insert when in string node - await testCompletionsFor('{ "a": "", "b" t| }', schema, { - count: 2, - items: [ - { label: 'true', resultText: '{ "a": "", "b": true }' }, + { label: '[]', resultText: '{ "c": "", "a" [$1] }' }, ] }); });