From fe868cda5e9c5718a60154fc607430dc19fb74c1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 28 Jul 2023 19:43:09 +0200 Subject: [PATCH 1/7] json5 insert text, prefer defaults --- src/json-completion.ts | 81 +++++++++++++++++++++++++++++++----------- src/json5-bundled.ts | 2 +- 2 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/json-completion.ts b/src/json-completion.ts index 7b069d6..d7becc9 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -22,6 +22,16 @@ import { jsonPointerForPosition } from "./utils/jsonPointers"; import { TOKENS } from "./constants"; import getSchema from "./utils/schema-lib/getSchema"; +function json5PropertyInsertSnippet(rawWord: string, value: string) { + if (rawWord.startsWith('"')) { + return `"${value}"`; + } + if (rawWord.startsWith("'")) { + return `'${value}'`; + } + return value; +} + class CompletionCollector { completions = new Map(); reservedKeys = new Set(); @@ -38,8 +48,15 @@ class CompletionCollector { } } +type JSONCompletionOptions = { + mode?: "json" | "json5"; +}; + export class JSONCompletion { - public constructor(private schema: JSONSchema7) {} + public constructor( + private schema: JSONSchema7, + private opts: JSONCompletionOptions + ) {} public doComplete(ctx: CompletionContext) { const result: CompletionResult = { @@ -66,6 +83,7 @@ export class JSONCompletion { } const currentWord = getWord(ctx.state.doc, node); + const rawWord = getWord(ctx.state.doc, node, false); // Calculate overwrite range if (node && (isPrimitiveValueNode(node) || isPropertyNameNode(node))) { result.from = node.from; @@ -134,7 +152,14 @@ export class JSONCompletion { } // property proposals with schema - this.getPropertyCompletions(this.schema, ctx, node, collector, addValue); + this.getPropertyCompletions( + this.schema, + ctx, + node, + collector, + addValue, + rawWord + ); } else { // proposals for values const types: { [type: string]: boolean } = {}; @@ -144,8 +169,10 @@ export class JSONCompletion { } // handle filtering - result.options = Array.from(collector.completions.values()).filter((v) => - stripSurrondingQuotes(v.label).startsWith(prefix) + result.options = Array.from(collector.completions.values()).filter( + (v) => + stripSurrondingQuotes(v.label).startsWith(prefix) || + stripSurrondingQuotes(v.label) === prefix ); debug.log( "xxx", @@ -174,7 +201,8 @@ export class JSONCompletion { ctx: CompletionContext, node: SyntaxNode, collector: CompletionCollector, - addValue: boolean + addValue: boolean, + rawWord: string ) { // don't suggest properties that are already present const properties = node.getChildren(TOKENS.PROPERTY); @@ -204,7 +232,12 @@ export class JSONCompletion { const completion: Completion = { // label is the unquoted key which will be displayed. label: key, - apply: this.getInsertTextForProperty(key, addValue, value), + apply: this.getInsertTextForProperty( + key, + addValue, + rawWord, + value + ), type: "property", detail: typeStr, info: description, @@ -221,7 +254,7 @@ export class JSONCompletion { if (label) { const completion: Completion = { label, - apply: this.getInsertTextForProperty(label, addValue), + apply: this.getInsertTextForProperty(label, addValue, rawWord), type: "property", }; collector.add(this.applySnippetCompletion(completion)); @@ -233,7 +266,7 @@ export class JSONCompletion { const label = propertyNames.const.toString(); const completion: Completion = { label, - apply: this.getInsertTextForProperty(label, addValue), + apply: this.getInsertTextForProperty(label, addValue, rawWord), type: "property", }; collector.add(this.applySnippetCompletion(completion)); @@ -256,6 +289,7 @@ export class JSONCompletion { private getInsertTextForProperty( key: string, addValue: boolean, + rawWord: string, propertySchema?: JSONSchema7Definition ) { // expand schema property if it is a reference @@ -263,7 +297,10 @@ export class JSONCompletion { ? this.expandSchemaProperty(propertySchema, this.schema) : propertySchema; - let resultText = `"${key}"`; + const isJSON5 = this.opts?.mode === "json5"; + let resultText = isJSON5 + ? json5PropertyInsertSnippet(rawWord, key) + : `"${key}"`; if (!addValue) { return resultText; } @@ -272,6 +309,13 @@ export class JSONCompletion { let value; let nValueProposals = 0; if (typeof propertySchema === "object") { + if (typeof propertySchema.default !== "undefined") { + console.log("default", propertySchema.default, value); + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.default, ""); + } + nValueProposals++; + } if (propertySchema.enum) { if (!value && propertySchema.enum.length === 1) { value = this.getInsertTextForGuessedValue(propertySchema.enum[0], ""); @@ -284,12 +328,6 @@ export class JSONCompletion { } nValueProposals++; } - if (typeof propertySchema.default !== "undefined") { - if (!value) { - value = this.getInsertTextForGuessedValue(propertySchema.default, ""); - } - nValueProposals++; - } if ( Array.isArray(propertySchema.examples) && propertySchema.examples.length @@ -302,7 +340,7 @@ export class JSONCompletion { } nValueProposals += propertySchema.examples.length; } - if (nValueProposals === 0) { + if (value === undefined && nValueProposals === 0) { let type = Array.isArray(propertySchema.type) ? propertySchema.type[0] : propertySchema.type; @@ -318,7 +356,7 @@ export class JSONCompletion { value = "#{}"; break; case "string": - value = '"#{}"'; + value = isJSON5 ? "'#{}'" : '"#{}"'; break; case "object": value = "{#{}}"; @@ -367,7 +405,7 @@ export class JSONCompletion { let snippetValue = JSON.stringify(value); snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } - return '"${' + snippetValue + '}"' + separatorAfter; + return '"${' + snippetValue + '}"'; } case "number": case "boolean": @@ -759,8 +797,11 @@ export class JSONCompletion { * provides a JSON schema enabled autocomplete extension for codemirror * @group Codemirror Extensions */ -export function jsonCompletion(schema: JSONSchema7) { - const completion = new JSONCompletion(schema); +export function jsonCompletion( + schema: JSONSchema7, + opts: JSONCompletionOptions = {} +) { + const completion = new JSONCompletion(schema, opts); return function jsonDoCompletion(ctx: CompletionContext) { return completion.doComplete(ctx); }; diff --git a/src/json5-bundled.ts b/src/json5-bundled.ts index 8fe4e89..4dba7f4 100644 --- a/src/json5-bundled.ts +++ b/src/json5-bundled.ts @@ -17,7 +17,7 @@ export function json5Schema(schema: JSONSchema7) { linter(json5ParseLinter()), linter(json5SchemaLinter(schema)), json5Language.data.of({ - autocomplete: jsonCompletion(schema), + autocomplete: jsonCompletion(schema, { mode: "json5" }), }), hoverTooltip(json5SchemaHover(schema)), ]; From 79cfe0c8fc33e12ee71744003f904c5eda1598d1 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Fri, 28 Jul 2023 19:52:00 +0200 Subject: [PATCH 2/7] update exports, docs --- README.md | 4 ++-- src/json-completion.ts | 14 ++++++++++++++ src/json5-bundled.ts | 4 ++-- src/json5.ts | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5bafce2..82be8d9 100644 --- a/README.md +++ b/README.md @@ -153,10 +153,10 @@ This approach allows you to configure the json5 mode and parse linter, as well a import { EditorState } from "@codemirror/state"; import { linter } from "@codemirror/lint"; import { json5, json5ParseLinter, json5Language } from "codemirror-json5"; -import { jsonCompletion } from "codemirror-json-schema"; import { json5SchemaLinter, json5SchemaHover, + json5Completion, } from "codemirror-json-schema/json5"; const schema = { @@ -182,7 +182,7 @@ const json5State = EditorState.create({ linter(json5SchemaLinter(schema)), hoverTooltip(json5SchemaHover(schema)), json5Language.data.of({ - autocomplete: jsonCompletion(schema), + autocomplete: json5Completion(schema), }), ], }); diff --git a/src/json-completion.ts b/src/json-completion.ts index d7becc9..0314469 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -806,3 +806,17 @@ export function jsonCompletion( return completion.doComplete(ctx); }; } + +/** + * provides a JSON schema enabled autocomplete extension for codemirror and json5 + * @group Codemirror Extensions + */ +export function json5Completion( + schema: JSONSchema7, + opts: Omit = {} +) { + const completion = new JSONCompletion(schema, { ...opts, mode: "json5" }); + return function jsonDoCompletion(ctx: CompletionContext) { + return completion.doComplete(ctx); + }; +} diff --git a/src/json5-bundled.ts b/src/json5-bundled.ts index 4dba7f4..e9d4c4b 100644 --- a/src/json5-bundled.ts +++ b/src/json5-bundled.ts @@ -1,7 +1,7 @@ import { JSONSchema7 } from "json-schema"; import { json5, json5Language, json5ParseLinter } from "codemirror-json5"; import { hoverTooltip } from "@codemirror/view"; -import { jsonCompletion } from "./json-completion"; +import { json5Completion } from "./json-completion"; import { json5SchemaLinter } from "./json5-validation"; import { json5SchemaHover } from "./json5-hover"; @@ -17,7 +17,7 @@ export function json5Schema(schema: JSONSchema7) { linter(json5ParseLinter()), linter(json5SchemaLinter(schema)), json5Language.data.of({ - autocomplete: jsonCompletion(schema, { mode: "json5" }), + autocomplete: json5Completion(schema), }), hoverTooltip(json5SchemaHover(schema)), ]; diff --git a/src/json5.ts b/src/json5.ts index 1383bf4..087e896 100644 --- a/src/json5.ts +++ b/src/json5.ts @@ -1,6 +1,7 @@ // json5 export { json5SchemaLinter } from "./json5-validation"; export { json5SchemaHover } from "./json5-hover"; +export { json5Completion } from "./json-completion"; /** * @group Bundled Codemirror Extensions From 754a60f4fd8c36aace31c753057dd9a374598a7d Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 29 Jul 2023 20:33:32 +0200 Subject: [PATCH 3/7] re-add seperatorAfter --- src/json-completion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/json-completion.ts b/src/json-completion.ts index 0314469..11a5193 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -405,7 +405,7 @@ export class JSONCompletion { let snippetValue = JSON.stringify(value); snippetValue = snippetValue.substr(1, snippetValue.length - 2); // remove quotes snippetValue = this.getInsertTextForPlainText(snippetValue); // escape \ and } - return '"${' + snippetValue + '}"'; + return '"${' + snippetValue + '}"' + separatorAfter; } case "number": case "boolean": From 84933baea34353ed9d944a414a0230d5a27cfe61 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 29 Jul 2023 20:35:49 +0200 Subject: [PATCH 4/7] if default is provided, only provide default, otherwise count valueProposals --- src/json-completion.ts | 112 +++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/src/json-completion.ts b/src/json-completion.ts index 11a5193..bc47ccc 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -315,64 +315,68 @@ export class JSONCompletion { value = this.getInsertTextForGuessedValue(propertySchema.default, ""); } nValueProposals++; - } - if (propertySchema.enum) { - if (!value && propertySchema.enum.length === 1) { - value = this.getInsertTextForGuessedValue(propertySchema.enum[0], ""); - } - nValueProposals += propertySchema.enum.length; - } - if (typeof propertySchema.const !== "undefined") { - if (!value) { - value = this.getInsertTextForGuessedValue(propertySchema.const, ""); + } else { + if (propertySchema.enum) { + if (!value && propertySchema.enum.length === 1) { + value = this.getInsertTextForGuessedValue( + propertySchema.enum[0], + "" + ); + } + nValueProposals += propertySchema.enum.length; } - nValueProposals++; - } - if ( - Array.isArray(propertySchema.examples) && - propertySchema.examples.length - ) { - if (!value) { - value = this.getInsertTextForGuessedValue( - propertySchema.examples[0], - "" - ); + if (typeof propertySchema.const !== "undefined") { + if (!value) { + value = this.getInsertTextForGuessedValue(propertySchema.const, ""); + } + nValueProposals++; } - nValueProposals += propertySchema.examples.length; - } - if (value === undefined && nValueProposals === 0) { - let type = Array.isArray(propertySchema.type) - ? propertySchema.type[0] - : propertySchema.type; - if (!type) { - if (propertySchema.properties) { - type = "object"; - } else if (propertySchema.items) { - type = "array"; + if ( + Array.isArray(propertySchema.examples) && + propertySchema.examples.length + ) { + if (!value) { + value = this.getInsertTextForGuessedValue( + propertySchema.examples[0], + "" + ); } + nValueProposals += propertySchema.examples.length; } - switch (type) { - case "boolean": - value = "#{}"; - break; - case "string": - value = isJSON5 ? "'#{}'" : '"#{}"'; - break; - case "object": - value = "{#{}}"; - break; - case "array": - value = "[#{}]"; - break; - case "number": - case "integer": - value = "#{0}"; - break; - case "null": - value = "#{null}"; - break; - default: - return resultText; + if (value === undefined && nValueProposals === 0) { + let type = Array.isArray(propertySchema.type) + ? propertySchema.type[0] + : propertySchema.type; + if (!type) { + if (propertySchema.properties) { + type = "object"; + } else if (propertySchema.items) { + type = "array"; + } + } + switch (type) { + case "boolean": + value = "#{}"; + break; + case "string": + value = isJSON5 ? "'#{}'" : '"#{}"'; + break; + case "object": + value = "{#{}}"; + break; + case "array": + value = "[#{}]"; + break; + case "number": + case "integer": + value = "#{0}"; + break; + case "null": + value = "#{null}"; + break; + default: + return resultText; + } } } } From 1b48c502758f706708f54eb67cec5f37e7de3396 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sat, 29 Jul 2023 20:38:01 +0200 Subject: [PATCH 5/7] spelling fix that i just noticed --- src/json-completion.ts | 10 ++++------ src/utils/node.ts | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/json-completion.ts b/src/json-completion.ts index bc47ccc..08deed8 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -14,7 +14,7 @@ import { getWord, isPropertyNameNode, isPrimitiveValueNode, - stripSurrondingQuotes, + stripSurroundingQuotes, getNodeAtPosition, } from "./utils/node"; import { Draft07, JsonError } from "json-schema-library"; @@ -169,10 +169,8 @@ export class JSONCompletion { } // handle filtering - result.options = Array.from(collector.completions.values()).filter( - (v) => - stripSurrondingQuotes(v.label).startsWith(prefix) || - stripSurrondingQuotes(v.label) === prefix + result.options = Array.from(collector.completions.values()).filter((v) => + stripSurroundingQuotes(v.label).startsWith(prefix) ); debug.log( "xxx", @@ -209,7 +207,7 @@ export class JSONCompletion { debug.log("xxx", "getPropertyCompletions", node, ctx, properties); properties.forEach((p) => { const key = getWord(ctx.state.doc, p.getChild(TOKENS.PROPERTY_NAME)); - collector.reserve(stripSurrondingQuotes(key)); + collector.reserve(stripSurroundingQuotes(key)); }); // TODO: Handle separatorAfter diff --git a/src/utils/node.ts b/src/utils/node.ts index b60039b..d554633 100644 --- a/src/utils/node.ts +++ b/src/utils/node.ts @@ -12,7 +12,7 @@ export const getNodeAtPosition = ( return syntaxTree(state).resolveInner(pos, side); }; -export const stripSurrondingQuotes = (str: string) => { +export const stripSurroundingQuotes = (str: string) => { return str.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1"); }; @@ -22,7 +22,7 @@ export const getWord = ( stripQuotes = true ) => { const word = node ? doc.sliceString(node.from, node.to) : ""; - return stripQuotes ? stripSurrondingQuotes(word) : word; + return stripQuotes ? stripSurroundingQuotes(word) : word; }; export const isInvalidValueNode = (node: SyntaxNode) => { From 4ac92680bf8753817f6c8f816a291c13582a59e8 Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 30 Jul 2023 12:02:26 +0200 Subject: [PATCH 6/7] add far more test coverage --- .vscode/settings.json | 4 +- src/__tests__/__helpers__/completion.ts | 21 +++-- src/__tests__/json-completion.spec.ts | 103 +++++++++++++++++++++++- src/json-completion.ts | 1 - 4 files changed, 118 insertions(+), 11 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 16876d3..68d96c6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, - "files.insertFinalNewline": true + "files.insertFinalNewline": true, + "coverage-gutters.coverageBaseDir": "**", + "coverage-gutters.coverageFileNames": ["clover.xml"] } diff --git a/src/__tests__/__helpers__/completion.ts b/src/__tests__/__helpers__/completion.ts index ffe6a76..bbcae18 100644 --- a/src/__tests__/__helpers__/completion.ts +++ b/src/__tests__/__helpers__/completion.ts @@ -1,6 +1,8 @@ import { describe, it, expect, vitest, Mock } from "vitest"; import { json, jsonLanguage } from "@codemirror/lang-json"; +import { json5, json5Language } from "codemirror-json5"; + import { EditorState } from "@codemirror/state"; import { Completion, @@ -38,19 +40,26 @@ type MockedCompletionResult = CompletionResult["options"][0] & { export async function expectCompletion( doc: string, results: MockedCompletionResult[], - schema?: JSONSchema7, - conf: { explicit?: boolean } = {} + + conf: { + explicit?: boolean; + schema?: JSONSchema7; + mode?: "json" | "json5"; + } = {} ) { let cur = doc.indexOf("|"), - currentSchema = schema || testSchema2; + currentSchema = conf?.schema || testSchema2; doc = doc.slice(0, cur) + doc.slice(cur + 1); + const jsonMode = conf?.mode === "json5" ? json5 : json; + const jsonLang = conf?.mode === "json5" ? json5Language : jsonLanguage; + let state = EditorState.create({ doc, selection: { anchor: cur }, extensions: [ - json(), - jsonLanguage.data.of({ - autocomplete: jsonCompletion(currentSchema), + jsonMode(), + jsonLang.data.of({ + autocomplete: jsonCompletion(currentSchema, { mode: conf.mode }), }), ], }); diff --git a/src/__tests__/json-completion.spec.ts b/src/__tests__/json-completion.spec.ts index 1f1ecdd..8686a91 100644 --- a/src/__tests__/json-completion.spec.ts +++ b/src/__tests__/json-completion.spec.ts @@ -46,9 +46,7 @@ describe("jsonCompletion", () => { type: "property", detail: "", info: "an example enum with default bar", - // TODO: should this not autocomplete to default "bar"? - // template: '"enum1": "${bar}"', - template: '"enum1": #{}', + template: '"enum1": "${bar}"', }, { label: "enum2", @@ -164,3 +162,102 @@ describe("jsonCompletion", () => { ]); }); }); + +describe("json5Completion", () => { + it("should return bare property key when no quotes are used", async () => { + await expectCompletion( + "{ f| }", + [ + { + label: "foo", + type: "property", + detail: "string", + info: "", + template: "foo: '#{}'", + }, + ], + { mode: "json5" } + ); + }); + it("should return template for '", async () => { + await expectCompletion( + "{ 'one|' }", + [ + { + label: "oneOfEg", + type: "property", + detail: "", + info: "an example oneOf", + template: "'oneOfEg': ", + }, + { + label: "oneOfEg2", + type: "property", + detail: "", + info: "", + template: "'oneOfEg2': ", + }, + { + detail: "", + info: "", + label: "oneOfObject", + template: "'oneOfObject': ", + type: "property", + }, + ], + { mode: "json5" } + ); + }); + it("should include defaults for enum when available", async () => { + await expectCompletion( + '{ "en|" }', + [ + { + label: "enum1", + type: "property", + detail: "", + info: "an example enum with default bar", + template: '"enum1": "${bar}"', + }, + { + label: "enum2", + type: "property", + detail: "", + info: "an example enum without default", + template: '"enum2": #{}', + }, + ], + { mode: "json5" } + ); + }); + it("should include defaults for boolean when available", async () => { + await expectCompletion( + "{ booleanW| }", + [ + { + type: "property", + detail: "boolean", + info: "an example boolean with default", + label: "booleanWithDefault", + template: "booleanWithDefault: ${true}", + }, + ], + { mode: "json5" } + ); + }); + it("should include insert text for nested object properties", async () => { + await expectCompletion( + "{ object: { f| }", + [ + { + type: "property", + detail: "string", + info: "an elegant string", + label: "foo", + template: "foo: '#{}'", + }, + ], + { mode: "json5" } + ); + }); +}); diff --git a/src/json-completion.ts b/src/json-completion.ts index 08deed8..5f7ec32 100644 --- a/src/json-completion.ts +++ b/src/json-completion.ts @@ -308,7 +308,6 @@ export class JSONCompletion { let nValueProposals = 0; if (typeof propertySchema === "object") { if (typeof propertySchema.default !== "undefined") { - console.log("default", propertySchema.default, value); if (!value) { value = this.getInsertTextForGuessedValue(propertySchema.default, ""); } From a138f764f5ac08627ac2c18f459bd58eb500570d Mon Sep 17 00:00:00 2001 From: Rikki Schulte Date: Sun, 6 Aug 2023 10:00:12 +0200 Subject: [PATCH 7/7] add more tests --- src/__tests__/json-completion.spec.ts | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/__tests__/json-completion.spec.ts b/src/__tests__/json-completion.spec.ts index 8686a91..2695efb 100644 --- a/src/__tests__/json-completion.spec.ts +++ b/src/__tests__/json-completion.spec.ts @@ -107,6 +107,18 @@ describe("jsonCompletion", () => { }, ]); }); + // TODO: accidentally steps up to the parent pointer + it.skip("should include insert text for nested object properties", async () => { + await expectCompletion(`{ "object": { '| } }`, [ + { + detail: "string", + info: "an elegant string", + label: "foo", + template: '"foo": "#{}"', + type: "property", + }, + ]); + }); it("should include insert text for nested object properties with filter", async () => { await expectCompletion('{ "object": { "f|" } }', [ { @@ -160,6 +172,38 @@ describe("jsonCompletion", () => { type: "property", }, ]); + it("should autocomplete for oneOf without quotes", async () => { + await expectCompletion('{ "oneOfObject": { | } }', [ + { + detail: "string", + info: "", + label: "foo", + template: '"foo": "#{}"', + type: "property", + }, + { + detail: "number", + info: "", + label: "bar", + template: '"bar": #{0}', + type: "property", + }, + { + detail: "string", + info: "", + label: "apple", + template: '"apple": "#{}"', + type: "property", + }, + { + detail: "number", + info: "", + label: "banana", + template: '"banana": #{0}', + type: "property", + }, + ]); + }); }); }); @@ -260,4 +304,40 @@ describe("json5Completion", () => { { mode: "json5" } ); }); + it("should include insert text for nested oneOf object properties with a single quote", async () => { + await expectCompletion( + "{ oneOfObject: { '|' }", + [ + { + type: "property", + detail: "string", + info: "", + label: "foo", + template: "'foo': '#{}'", + }, + { + type: "property", + detail: "number", + info: "", + label: "bar", + template: "'bar': #{0}", + }, + { + type: "property", + detail: "string", + info: "", + label: "apple", + template: "'apple': '#{}'", + }, + { + type: "property", + detail: "number", + info: "", + label: "banana", + template: "'banana': #{0}", + }, + ], + { mode: "json5" } + ); + }); });