Skip to content

Commit 296617f

Browse files
authored
Improvements to completion logic (mainly for top level) (#102)
Improvements to completion logic (mainly for top level). In true TDD fashion, I'm fairly confident with the changes given the tests are all passing. Added additional tests for - curly brace in default behavior #96 - autocomplete for a schema with top level $ref - autocomplete for a schema with top level complex type I wanted to also work on the "include value completions for boolean" test case but there's a bit extra work (when completing the property value and the node used for the completion is different from the original node passed in to `getValueCompletions()`, we also need to figure out the correct `from` and `to` values for the completion context, so that applying the completion replaces the correct things, otherwise it will replace the original node which may not be what we want) to be done to get it working as expected, so decided to tackle part of it but leave it commented out for now.
1 parent c63fc22 commit 296617f

File tree

6 files changed

+148
-28
lines changed

6 files changed

+148
-28
lines changed

.changeset/perfect-balloons-teach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"codemirror-json-schema": patch
3+
---
4+
5+
Improvements to completion logic (mainly for top level)

src/__tests__/__fixtures__/schemas.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ export const testSchema2 = {
1717
foo: {
1818
type: "string",
1919
},
20+
stringWithDefault: {
21+
type: "string",
22+
description: "a string with a default value",
23+
default: "defaultString",
24+
},
25+
bracedStringDefault: {
26+
type: "string",
27+
description: "a string with a default value containing braces",
28+
default: "✨ A message from %{whom}: ✨",
29+
},
2030
object: {
2131
type: "object",
2232
properties: {

src/__tests__/__helpers__/completion.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export async function expectCompletion(
4747
} = {}
4848
) {
4949
let cur = doc.indexOf("|"),
50-
currentSchema = conf?.schema || testSchema2;
50+
currentSchema = conf?.schema ?? testSchema2;
5151
doc = doc.slice(0, cur) + doc.slice(cur + 1);
5252

5353
let state = EditorState.create({

src/__tests__/json-completion.spec.ts

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it } from "vitest";
22

33
import { expectCompletion } from "./__helpers__/completion.js";
44
import { MODES } from "../constants.js";
5+
import { testSchema3, testSchema4 } from "./__fixtures__/schemas.js";
56

67
describe.each([
78
{
@@ -46,6 +47,35 @@ describe.each([
4647
},
4748
],
4849
},
50+
{
51+
name: "include defaults for string when available",
52+
mode: MODES.JSON,
53+
docs: ['{ "stringWithDefault| }'],
54+
expectedResults: [
55+
{
56+
label: "stringWithDefault",
57+
type: "property",
58+
detail: "string",
59+
info: "a string with a default value",
60+
template: '"stringWithDefault": "${defaultString}"',
61+
},
62+
],
63+
},
64+
// TODO: fix the default template with braces: https://discuss.codemirror.net/t/inserting-literal-via-snippets/8136/4
65+
// {
66+
// name: "include defaults for string with braces",
67+
// mode: MODES.JSON,
68+
// docs: ['{ "bracedStringDefault| }'],
69+
// expectedResults: [
70+
// {
71+
// label: "bracedStringDefault",
72+
// type: "property",
73+
// detail: "string",
74+
// info: "a string with a default value containing braces",
75+
// template: '"bracedStringDefault": "${✨ A message from %{whom}: ✨}"',
76+
// },
77+
// ],
78+
// },
4979
{
5080
name: "include defaults for enum when available",
5181
mode: MODES.JSON,
@@ -110,13 +140,11 @@ describe.each([
110140
},
111141
],
112142
},
113-
// TODO: should provide true/false completions
143+
// TODO: should provide true/false completions. Issue is the detected node is the Property node, which contains the property name and value. The prefix for the autocompletion therefore contains the property name, so it never matches the results
114144
// {
115145
// name: "include value completions for boolean",
116-
// mode: MODES.JSON,
117-
// docs: ['{ "booleanWithDefault": | }',
118-
// },
119-
// ],
146+
// mode: MODES.JSON,
147+
// docs: ['{ "booleanWithDefault": | }'],
120148
// expectedResults: [
121149
// {
122150
// detail: "boolean",
@@ -221,6 +249,20 @@ describe.each([
221249
],
222250
},
223251
// TODO: completion for array of objects should enhance the template
252+
{
253+
name: "autocomplete for array of objects with filter",
254+
mode: MODES.JSON,
255+
docs: ['{ "arrayOfObjects": [ { "f|" } ] }'],
256+
expectedResults: [
257+
{
258+
detail: "string",
259+
info: "",
260+
label: "foo",
261+
template: '"foo": "#{}"',
262+
type: "property",
263+
},
264+
],
265+
},
224266
{
225267
name: "autocomplete for array of objects with items",
226268
mode: MODES.JSON,
@@ -298,6 +340,50 @@ describe.each([
298340
},
299341
],
300342
},
343+
{
344+
name: "autocomplete for a schema with top level $ref",
345+
mode: MODES.JSON,
346+
docs: ['{ "| }'],
347+
expectedResults: [
348+
{
349+
type: "property",
350+
detail: "string",
351+
info: "",
352+
label: "foo",
353+
template: '"foo": "#{}"',
354+
},
355+
{
356+
type: "property",
357+
detail: "number",
358+
info: "",
359+
label: "bar",
360+
template: '"bar": #{0}',
361+
},
362+
],
363+
schema: testSchema3,
364+
},
365+
{
366+
name: "autocomplete for a schema with top level complex type",
367+
mode: MODES.JSON,
368+
docs: ['{ "| }'],
369+
expectedResults: [
370+
{
371+
type: "property",
372+
detail: "string",
373+
info: "",
374+
label: "foo",
375+
template: '"foo": "#{}"',
376+
},
377+
{
378+
type: "property",
379+
detail: "number",
380+
info: "",
381+
label: "bar",
382+
template: '"bar": #{0}',
383+
},
384+
],
385+
schema: testSchema4,
386+
},
301387
// JSON5
302388
{
303389
name: "return bare property key when no quotes are used",
@@ -653,8 +739,8 @@ describe.each([
653739
},
654740
],
655741
},
656-
])("jsonCompletion", ({ name, docs, mode, expectedResults }) => {
742+
])("jsonCompletion", ({ name, docs, mode, expectedResults, schema }) => {
657743
it.each(docs)(`${name} (mode: ${mode})`, async (doc) => {
658-
await expectCompletion(doc, expectedResults, { mode });
744+
await expectCompletion(doc, expectedResults, { mode, schema });
659745
});
660746
});

src/json-completion.ts

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class JSONCompletion {
5858
this.mode = opts.mode ?? MODES.JSON;
5959
}
6060
public doComplete(ctx: CompletionContext) {
61-
this.schema = getJSONSchema(ctx.state)!;
61+
const s = getJSONSchema(ctx.state)!;
62+
this.schema = this.expandSchemaProperty(s, s) ?? s;
6263
if (!this.schema) {
6364
// todo: should we even do anything without schema
6465
// without taking over the existing mode responsibilties?
@@ -76,7 +77,7 @@ export class JSONCompletion {
7677
let node: SyntaxNode | null = getNodeAtPosition(ctx.state, ctx.pos);
7778

7879
// position node word prefix (without quotes) for matching
79-
const prefix = ctx.state.sliceDoc(node.from, ctx.pos).replace(/^("|')/, "");
80+
let prefix = ctx.state.sliceDoc(node.from, ctx.pos).replace(/^(["'])/, "");
8081

8182
debug.log("xxx", "node", node, "prefix", prefix, "ctx", ctx);
8283

@@ -212,7 +213,14 @@ export class JSONCompletion {
212213
const types: { [type: string]: boolean } = {};
213214

214215
// value proposals with schema
215-
this.getValueCompletions(this.schema, ctx, types, collector);
216+
const res = this.getValueCompletions(this.schema, ctx, types, collector);
217+
debug.log("xxx", "getValueCompletions res", res);
218+
if (res) {
219+
// TODO: While this works, we also need to handle the completion from and to positions to use it
220+
// // use the value node to calculate the prefix
221+
// prefix = res.valuePrefix;
222+
// debug.log("xxx", "using valueNode prefix", prefix);
223+
}
216224
}
217225

218226
// handle filtering
@@ -269,6 +277,7 @@ export class JSONCompletion {
269277

270278
// Get matching schemas
271279
const schemas = this.getSchemas(schema, ctx);
280+
debug.log("xxx", "propertyCompletion schemas", schemas);
272281

273282
schemas.forEach((s) => {
274283
if (typeof s !== "object") {
@@ -458,15 +467,7 @@ export class JSONCompletion {
458467
}
459468
private getInsertTextForPropertyName(key: string, rawWord: string) {
460469
switch (this.mode) {
461-
case MODES.JSON5: {
462-
if (rawWord.startsWith('"')) {
463-
return `"${key}"`;
464-
}
465-
if (rawWord.startsWith("'")) {
466-
return `'${key}'`;
467-
}
468-
return key;
469-
}
470+
case MODES.JSON5:
470471
case MODES.YAML: {
471472
if (rawWord.startsWith('"')) {
472473
return `"${key}"`;
@@ -484,13 +485,10 @@ export class JSONCompletion {
484485
switch (this.mode) {
485486
case MODES.JSON5:
486487
return `'${prf}{${value}}'`;
487-
break;
488488
case MODES.YAML:
489489
return `${prf}{${value}}`;
490-
break;
491490
default:
492491
return `"${prf}{${value}}"`;
493-
break;
494492
}
495493
}
496494

@@ -661,6 +659,19 @@ export class JSONCompletion {
661659
}
662660
}
663661
}
662+
663+
// TODO: We need to pass the from and to for the value node as well
664+
// TODO: What should be the from and to when the value node is null?
665+
// TODO: (NOTE: if we pass a prefix but no from and to, it will autocomplete the value but replace
666+
// TODO: the entire property nodewhich isn't what we want). Instead we need to change the from and to
667+
// TODO: based on the corresponding (relevant) value node
668+
const valuePrefix = valueNode
669+
? getWord(ctx.state.doc, valueNode, true, false)
670+
: "";
671+
672+
return {
673+
valuePrefix,
674+
};
664675
}
665676

666677
private addSchemaValueCompletions(
@@ -816,8 +827,9 @@ export class JSONCompletion {
816827
debug.log("xxx", "pointer..", JSON.stringify(pointer));
817828

818829
// For some reason, it returns undefined schema for the root pointer
830+
// We use the root schema in that case as the relevant (sub)schema
819831
if (!pointer || pointer === "/") {
820-
return [schema];
832+
subSchema = this.expandSchemaProperty(schema, schema) ?? schema;
821833
}
822834
// const subSchema = new Draft07(this.schema).getSchema(pointer);
823835
debug.log("xxx", "subSchema..", subSchema);
@@ -847,8 +859,8 @@ export class JSONCompletion {
847859
return [subSchema as JSONSchema7];
848860
}
849861

850-
private expandSchemaProperty(
851-
property: JSONSchema7Definition,
862+
private expandSchemaProperty<T extends JSONSchema7Definition>(
863+
property: T,
852864
schema: JSONSchema7
853865
) {
854866
if (typeof property === "object" && property.$ref) {

src/utils/node.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,17 @@ export const surroundingDoubleQuotesToSingle = (str: string) => {
2323
export const getWord = (
2424
doc: Text,
2525
node: SyntaxNode | null,
26-
stripQuotes = true
26+
stripQuotes = true,
27+
onlyEvenQuotes = true
2728
) => {
2829
const word = node ? doc.sliceString(node.from, node.to) : "";
29-
return stripQuotes ? stripSurroundingQuotes(word) : word;
30+
if (!stripQuotes) {
31+
return word;
32+
}
33+
if (onlyEvenQuotes) {
34+
return stripSurroundingQuotes(word);
35+
}
36+
return word.replace(/(^["'])|(["']$)/g, "");
3037
};
3138

3239
export const isInvalidValueNode = (node: SyntaxNode, mode: JSONMode) => {

0 commit comments

Comments
 (0)