skir-codemirror-plugin 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +126 -0
  2. package/dist/codemirror/create_editor_state.d.ts +20 -0
  3. package/dist/codemirror/create_editor_state.d.ts.map +1 -0
  4. package/dist/codemirror/create_editor_state.js +252 -0
  5. package/dist/codemirror/create_editor_state.js.map +1 -0
  6. package/dist/codemirror/enter_key_handler.d.ts +8 -0
  7. package/dist/codemirror/enter_key_handler.d.ts.map +1 -0
  8. package/dist/codemirror/enter_key_handler.js +181 -0
  9. package/dist/codemirror/enter_key_handler.js.map +1 -0
  10. package/dist/codemirror/json_completion.d.ts +4 -0
  11. package/dist/codemirror/json_completion.d.ts.map +1 -0
  12. package/dist/codemirror/json_completion.js +150 -0
  13. package/dist/codemirror/json_completion.js.map +1 -0
  14. package/dist/codemirror/json_linter.d.ts +4 -0
  15. package/dist/codemirror/json_linter.d.ts.map +1 -0
  16. package/dist/codemirror/json_linter.js +277 -0
  17. package/dist/codemirror/json_linter.js.map +1 -0
  18. package/dist/codemirror/json_state.d.ts +16 -0
  19. package/dist/codemirror/json_state.d.ts.map +1 -0
  20. package/dist/codemirror/json_state.js +124 -0
  21. package/dist/codemirror/json_state.js.map +1 -0
  22. package/dist/codemirror/status_bar.d.ts +3 -0
  23. package/dist/codemirror/status_bar.d.ts.map +1 -0
  24. package/dist/codemirror/status_bar.js +123 -0
  25. package/dist/codemirror/status_bar.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +2 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/json/json_parser.d.ts +3 -0
  31. package/dist/json/json_parser.d.ts.map +1 -0
  32. package/dist/json/json_parser.js +414 -0
  33. package/dist/json/json_parser.js.map +1 -0
  34. package/dist/json/json_parser.test.d.ts +2 -0
  35. package/dist/json/json_parser.test.d.ts.map +1 -0
  36. package/dist/json/json_parser.test.js +337 -0
  37. package/dist/json/json_parser.test.js.map +1 -0
  38. package/dist/json/schema_validator.d.ts +3 -0
  39. package/dist/json/schema_validator.d.ts.map +1 -0
  40. package/dist/json/schema_validator.js +525 -0
  41. package/dist/json/schema_validator.js.map +1 -0
  42. package/dist/json/schema_validator.test.d.ts +2 -0
  43. package/dist/json/schema_validator.test.d.ts.map +1 -0
  44. package/dist/json/schema_validator.test.js +212 -0
  45. package/dist/json/schema_validator.test.js.map +1 -0
  46. package/dist/json/to_json.d.ts +6 -0
  47. package/dist/json/to_json.d.ts.map +1 -0
  48. package/dist/json/to_json.js +61 -0
  49. package/dist/json/to_json.js.map +1 -0
  50. package/dist/json/to_json.test.d.ts +2 -0
  51. package/dist/json/to_json.test.d.ts.map +1 -0
  52. package/dist/json/to_json.test.js +128 -0
  53. package/dist/json/to_json.test.js.map +1 -0
  54. package/dist/json/types.d.ts +170 -0
  55. package/dist/json/types.d.ts.map +1 -0
  56. package/dist/json/types.js +2 -0
  57. package/dist/json/types.js.map +1 -0
  58. package/package.json +85 -0
  59. package/src/codemirror/create_editor_state.ts +278 -0
  60. package/src/codemirror/enter_key_handler.ts +232 -0
  61. package/src/codemirror/json_completion.ts +182 -0
  62. package/src/codemirror/json_linter.ts +358 -0
  63. package/src/codemirror/json_state.ts +170 -0
  64. package/src/codemirror/status_bar.ts +137 -0
  65. package/src/index.ts +6 -0
  66. package/src/json/json_parser.test.ts +360 -0
  67. package/src/json/json_parser.ts +461 -0
  68. package/src/json/schema_validator.test.ts +230 -0
  69. package/src/json/schema_validator.ts +558 -0
  70. package/src/json/to_json.test.ts +150 -0
  71. package/src/json/to_json.ts +70 -0
  72. package/src/json/types.ts +254 -0
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/json/types.ts"],"names":[],"mappings":"","sourcesContent":["// To use instead of `any`\nexport type Json =\n | null\n | boolean\n | number\n | string\n | readonly Json[]\n | Readonly<{ [name: string]: Json }>;\n\n// -----------------------------------------------------------------------------\n// ERRORS\n// -----------------------------------------------------------------------------\n\nexport interface JsonError {\n readonly kind: \"error\";\n readonly segment: Segment;\n readonly message: string;\n}\n\n// -----------------------------------------------------------------------------\n// PARSING\n// -----------------------------------------------------------------------------\n\nexport interface JsonParseResult {\n /// If `undefined`, then `errors` is guaranteed not to be empty.\n readonly value: JsonValue | undefined;\n readonly errors: readonly JsonError[];\n /// Set of edits to apply to the original JSON code to fix formatting and\n // comma errors (no comma between consecutive values, trailing comma).\n readonly edits: readonly JsonEdit[];\n}\n\nexport type JsonValue = JsonArray | JsonObject | JsonLiteral;\n\nexport interface JsonArray {\n readonly kind: \"array\";\n /// Position of the '[' token.\n readonly firstToken: Segment;\n /// From '[' to ']' included.\n readonly segment: Segment;\n readonly values: JsonValue[];\n expectedType?: TypeSignature;\n}\n\nexport interface JsonKey {\n readonly keySegment: Segment;\n readonly key: string;\n}\n\nexport interface JsonKeyValue {\n readonly keySegment: Segment;\n readonly key: string;\n readonly value: JsonValue;\n expectedType?: TypeSignature;\n}\n\nexport interface JsonObject {\n readonly kind: \"object\";\n /// Position of the '{' token.\n readonly firstToken: Segment;\n /// From '{' to '}' included.\n readonly segment: Segment;\n readonly keyValues: { [key: string]: JsonKeyValue };\n /// Includes \"broken\" keys which produced a parsing error.\n readonly allKeys: readonly JsonKey[];\n expectedType?: TypeSignature;\n}\n\nexport interface JsonLiteral {\n readonly kind: \"literal\";\n /// Position of the first and only token. Same as `segment`.\n readonly firstToken: Segment;\n readonly segment: Segment;\n readonly jsonCode: string;\n readonly type: \"boolean\" | \"null\" | \"number\" | \"string\";\n expectedType?: TypeSignature;\n}\n\nexport interface Segment {\n readonly start: number;\n readonly end: number;\n}\n\n/// Path to a sub-value within a valid Skir value.\nexport type Path =\n | {\n readonly kind: \"root\";\n }\n | {\n readonly kind: \"field-value\";\n /// Name of the field of the struct\n readonly fieldName: string;\n /// Path to the struct value\n readonly structPath: Path;\n }\n | {\n readonly kind: \"variant-value\";\n /// Name of the wrapper variant of the enum\n readonly variantName: string;\n /// Path to the enum value\n readonly enumPath: Path;\n }\n | {\n readonly kind: \"array-item\";\n readonly index: number;\n readonly key: string | null;\n readonly arrayPath: Path;\n };\n\nexport interface JsonValueContext {\n readonly value: JsonValue;\n readonly path: Path;\n}\n\nexport interface JsonEdit {\n readonly segment: Segment;\n readonly replacement: string;\n}\n\n// -----------------------------------------------------------------------------\n// SCHEMA\n// -----------------------------------------------------------------------------\n\n/** JSON representation of a `TypeDescriptor`. */\nexport type TypeDefinition = {\n readonly type: TypeSignature;\n readonly records: readonly RecordDefinition[];\n};\n\n/** A type in the JSON representation of a `TypeDescriptor`. */\nexport type TypeSignature =\n | {\n kind: \"optional\";\n value: TypeSignature;\n }\n | ArrayTypeSignature\n | RecordTypeSignature\n | {\n kind: \"primitive\";\n value: PrimitiveType;\n };\n\nexport interface ArrayTypeSignature {\n readonly kind: \"array\";\n readonly value: {\n readonly item: TypeSignature;\n readonly key_extractor?: string;\n };\n}\n\nexport interface RecordTypeSignature {\n readonly kind: \"record\";\n readonly value: string;\n}\n\nexport type PrimitiveType =\n | \"bool\"\n | \"int32\"\n | \"int64\"\n | \"hash64\"\n | \"float32\"\n | \"float64\"\n | \"timestamp\"\n | \"string\"\n | \"bytes\";\n\nexport type RecordDefinition = StructDefinition | EnumDefinition;\n\n/** Definition of a struct in the JSON representation of a `TypeDescriptor`. */\nexport type StructDefinition = {\n readonly kind: \"struct\";\n readonly id: string;\n readonly doc?: string;\n readonly fields: readonly FieldDefinition[];\n readonly removed_numbers?: readonly number[];\n};\n\n/**\n * Definition of a struct field in the JSON representation of a\n * `TypeDescriptor`.\n */\nexport type FieldDefinition = {\n readonly name: string;\n readonly type: TypeSignature;\n readonly number: number;\n readonly doc?: string;\n};\n\n/** Definition of an enum in the JSON representation of a `TypeDescriptor`. */\nexport type EnumDefinition = {\n readonly kind: \"enum\";\n readonly id: string;\n readonly doc?: string;\n readonly variants: readonly VariantDefinition[];\n readonly removed_numbers?: readonly number[];\n};\n\n/**\n * Definition of an enum variant in the JSON representation of a\n * `TypeDescriptor`.\n */\nexport type VariantDefinition = {\n readonly name: string;\n readonly type?: TypeSignature;\n readonly number: number;\n readonly doc?: string;\n};\n\n// -----------------------------------------------------------------------------\n// SCHEMA VALIDATION\n// -----------------------------------------------------------------------------\n\nexport interface ValidationResult {\n readonly errors: JsonError[];\n readonly hints: Hint[];\n readonly rootTypeHint: TypeHint | undefined;\n readonly pathToTypeHint: ReadonlyMap<Path, TypeHint>;\n}\n\nexport interface TypeHint {\n readonly segment: Segment;\n readonly message: string | readonly string[];\n readonly valueContext: JsonValueContext;\n /// In order. All are included in 'valueContext.value.segment'.\n readonly childHints: readonly TypeHint[];\n}\n\nexport type Hint =\n | {\n readonly segment: Segment;\n readonly message: string;\n readonly valueContext?: undefined;\n }\n | TypeHint;\n\nexport interface MutableTypeHint extends TypeHint {\n readonly childHints: TypeHint[];\n}\n\n// -----------------------------------------------------------------------------\n// METHOD LIST\n// -----------------------------------------------------------------------------\n\nexport interface MethodList {\n readonly methods: readonly Method[];\n}\n\nexport interface Method {\n readonly method: string;\n readonly number: number | string;\n readonly request: TypeDefinition;\n readonly response: TypeDefinition;\n readonly doc?: string;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "skir-codemirror-plugin",
3
+ "version": "0.9.0",
4
+ "description": "",
5
+ "homepage": "https://github.com/gepheum/skir-codemirror-plugin#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/gepheum/skir-codemirror-plugin/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/gepheum/skir-codemirror-plugin.git"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "src/",
16
+ "README.md"
17
+ ],
18
+ "license": "ISC",
19
+ "author": "Tyler Fibonacci <gepheum@gmail.com>",
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "type": "module",
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "build:dev": "tsc -p tsconfig.dev.json",
33
+ "build:watch": "tsc --watch",
34
+ "build:dev:watch": "tsc -p tsconfig.dev.json --watch",
35
+ "clean": "rimraf dev-dist my-element.{d.ts,d.ts.map,js,js.map} test/my-element.{d.ts,d.ts.map,js,js.map} test/my-element_test.{d.ts,d.ts.map,js,js.map}",
36
+ "lint": "eslint src/**/*.ts && lit-analyzer",
37
+ "format": "prettier \"src/**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.prettierignore --write",
38
+ "format:check": "prettier \"src/**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.prettierignore --check",
39
+ "analyze": "cem analyze --litelement --globs \"src/**/*.ts\"",
40
+ "analyze:watch": "cem analyze --litelement --globs \"src/**/*.ts\" --watch",
41
+ "dev": "npm run build && npm run build:dev && concurrently -k \"npm:build:watch\" \"npm:build:dev:watch\" \"npm:dev:serve\"",
42
+ "dev:serve": "wds --watch --open /dev/index.html",
43
+ "serve": "npm run build && wds --watch",
44
+ "test": "mocha",
45
+ "prepublishOnly": "npm run build"
46
+ },
47
+ "dependencies": {
48
+ "@codemirror/buildhelper": "^1.0.2",
49
+ "@codemirror/lang-json": "^6.0.2",
50
+ "@codemirror/language": "^6.11.2",
51
+ "@uiw/codemirror-theme-tokyo-night": "^4.25.4",
52
+ "@uiw/codemirror-theme-tokyo-night-day": "^4.25.9",
53
+ "codemirror": "^6.0.2",
54
+ "lit": "^3.2.0",
55
+ "nanoevents": "^9.1.0",
56
+ "skir-client": "^1.0.11"
57
+ },
58
+ "devDependencies": {
59
+ "@custom-elements-manifest/analyzer": "^0.6.3",
60
+ "@eslint/js": "^9.39.2",
61
+ "@types/mocha": "^10.0.1",
62
+ "@web/dev-server": "^0.4.6",
63
+ "@web/dev-server-import-maps": "^0.2.1",
64
+ "@web/dev-server-legacy": "^1.0.0",
65
+ "@webcomponents/webcomponentsjs": "^2.8.0",
66
+ "buckwheat": "^1.1.2",
67
+ "concurrently": "^9.2.1",
68
+ "eslint": "^10.0.1",
69
+ "lit-analyzer": "^2.0.3",
70
+ "mocha": "^11.7.5",
71
+ "prettier": "^2.8.8",
72
+ "prettier-plugin-organize-imports": "^4.2.0",
73
+ "rimraf": "^6.1.3",
74
+ "ts-node": "^10.9.2",
75
+ "tsx": "^4.21.0",
76
+ "typescript": "~5.5.0",
77
+ "typescript-eslint": "^8.56.0"
78
+ },
79
+ "overrides": {
80
+ "diff": "^8.0.3",
81
+ "minimatch": "^10.2.1",
82
+ "serialize-javascript": "^7.0.4"
83
+ },
84
+ "customElements": "custom-elements.json"
85
+ }
@@ -0,0 +1,278 @@
1
+ import { autocompletion, closeBrackets } from "@codemirror/autocomplete";
2
+ import { json as jsonExtension } from "@codemirror/lang-json";
3
+ import { linter, lintGutter } from "@codemirror/lint";
4
+ import { EditorState, Extension } from "@codemirror/state";
5
+ import { EditorView } from "@codemirror/view";
6
+ import { tokyoNight } from "@uiw/codemirror-theme-tokyo-night";
7
+ import { tokyoNightDay } from "@uiw/codemirror-theme-tokyo-night-day";
8
+ import { basicSetup } from "codemirror";
9
+ import { makeJsonTemplate } from "../json/to_json";
10
+ import type { Json, RecordDefinition, TypeDefinition } from "../json/types";
11
+ import { enterKeyHandler } from "./enter_key_handler";
12
+ import { jsonCompletion } from "./json_completion";
13
+ import { jsonLinter } from "./json_linter";
14
+ import { debouncedJsonParser } from "./json_state";
15
+ import { statusBar } from "./status_bar";
16
+
17
+ export type CreateEditorStateParams = {
18
+ schema: TypeDefinition;
19
+ readOnly?: true;
20
+ json?: Json;
21
+ theme?: "tokyo-night" | "tokyo-night-day" | CustomTheme;
22
+ };
23
+
24
+ export function createEditorState({
25
+ schema,
26
+ readOnly,
27
+ json,
28
+ theme,
29
+ }: CreateEditorStateParams): EditorState {
30
+ const idToRecordDef: { [id: string]: RecordDefinition } = {};
31
+ for (const record of schema.records) {
32
+ idToRecordDef[record.id] = record;
33
+ }
34
+ const content = json ?? makeJsonTemplate(schema.type, idToRecordDef);
35
+
36
+ switch (theme) {
37
+ case undefined:
38
+ case "tokyo-night": {
39
+ theme = {
40
+ backgroundColor: "#1a1b26",
41
+ lighterBgColor: "#1f2335",
42
+ borderColor: "#414868",
43
+ foregroundColor: "#c0caf5",
44
+ accentColor: "#7aa2f7",
45
+ errorColor: "#f7768e",
46
+ selectionColor: "#515c7e40",
47
+ themeExtension: tokyoNight,
48
+ };
49
+ break;
50
+ }
51
+ case "tokyo-night-day": {
52
+ theme = {
53
+ backgroundColor: "#d5d6db",
54
+ lighterBgColor: "#e1e2e7",
55
+ borderColor: "#adb0bb",
56
+ foregroundColor: "#3760bf",
57
+ accentColor: "#2e7de9",
58
+ errorColor: "#f52a65",
59
+ selectionColor: "#3760bf33",
60
+ themeExtension: tokyoNightDay,
61
+ };
62
+ break;
63
+ }
64
+ }
65
+
66
+ return EditorState.create({
67
+ doc: JSON.stringify(content, null, 2),
68
+ extensions: [
69
+ EditorState.readOnly.of(!!readOnly),
70
+ enterKeyHandler(schema),
71
+ basicSetup,
72
+ EditorState.languageData.of(() => [
73
+ {
74
+ closeBrackets: { before: ",]}" },
75
+ },
76
+ ]),
77
+ closeBrackets(),
78
+ theme.themeExtension ?? [],
79
+ jsonExtension(),
80
+ debouncedJsonParser(schema),
81
+ linter(jsonLinter(readOnly ? "read-only" : "editable")),
82
+ autocompletion({
83
+ override: [jsonCompletion(schema)],
84
+ }),
85
+ lintGutter(),
86
+ statusBar(),
87
+ EditorView.theme({
88
+ "&": {
89
+ fontSize: "14px",
90
+ height: "100%",
91
+ },
92
+ ".cm-scroller": {
93
+ fontFamily: "'JetBrains Mono', monospace",
94
+ overflow: "auto",
95
+ },
96
+ ".cm-lintRange-info": {
97
+ backgroundImage: "none",
98
+ },
99
+ ".cm-lintRange-info:hover": {
100
+ backgroundColor: theme.selectionColor,
101
+ },
102
+ ".cm-lintRange-error": {
103
+ backgroundImage: "none",
104
+ borderBottom: `3px solid ${theme.errorColor}`,
105
+ },
106
+ ".cm-tooltip-hover": {
107
+ backgroundColor: theme.lighterBgColor,
108
+ border: `1px solid ${theme.borderColor}`,
109
+ borderRadius: "4px",
110
+ padding: "8px 12px",
111
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.5)",
112
+ fontSize: "14px",
113
+ lineHeight: "1.4",
114
+ color: theme.foregroundColor,
115
+ },
116
+ ".cm-tooltip.cm-tooltip-lint": {
117
+ backgroundColor: theme.lighterBgColor,
118
+ border: `1px solid ${theme.borderColor}`,
119
+ borderRadius: "4px",
120
+ padding: "8px 12px",
121
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.5)",
122
+ fontSize: "14px", // Ensure consistency
123
+ color: theme.foregroundColor,
124
+ },
125
+ ".cm-tooltip-lint .cm-diagnostic-error": {
126
+ fontWeight: "bold",
127
+ color: theme.errorColor,
128
+ },
129
+ ".cm-status-bar": {
130
+ backgroundColor: theme.lighterBgColor,
131
+ borderTopColor: theme.borderColor,
132
+ color: theme.accentColor,
133
+ },
134
+ ".cm-diagnostic-textarea": {
135
+ backgroundColor: theme.lighterBgColor,
136
+ borderColor: theme.borderColor,
137
+ color: theme.foregroundColor,
138
+ },
139
+ ".cm-diagnostic-textarea[readonly]": {
140
+ backgroundColor: theme.backgroundColor,
141
+ },
142
+ ".cm-diagnostic-input": {
143
+ backgroundColor: theme.lighterBgColor,
144
+ borderColor: theme.borderColor,
145
+ color: theme.foregroundColor,
146
+ },
147
+ ".cm-diagnostic-button": {
148
+ backgroundColor: theme.lighterBgColor,
149
+ borderColor: theme.borderColor,
150
+ color: theme.foregroundColor,
151
+ },
152
+ ".cm-diagnostic-button:hover": {
153
+ backgroundColor: theme.borderColor,
154
+ },
155
+ ".cm-diagnostic-error-message": {
156
+ color: theme.errorColor,
157
+ },
158
+ ".diagnostic-row + .diagnostic-row": {
159
+ borderTopColor: theme.borderColor,
160
+ },
161
+ }),
162
+ EditorView.baseTheme({
163
+ ".cm-lint-marker-info": {
164
+ display: "none",
165
+ },
166
+ ".cm-status-bar": {
167
+ display: "flex",
168
+ flexDirection: "row-reverse",
169
+ justifyContent: "flex-end",
170
+ padding: "4px 12px",
171
+ borderTop: "1px solid",
172
+ fontSize: "12px",
173
+ height: "16px",
174
+ },
175
+ ".cm-status-bar-link": {
176
+ textDecoration: "none",
177
+ cursor: "pointer",
178
+ },
179
+ ".cm-status-bar-link:hover": {
180
+ textDecoration: "underline",
181
+ },
182
+ ".cm-status-bar-link:hover ~ .cm-status-bar-link": {
183
+ textDecoration: "underline",
184
+ },
185
+ ".cm-diagnostic-wrapper": {
186
+ fontSize: "12px",
187
+ lineHeight: "1.3",
188
+ padding: "2px",
189
+ },
190
+ ".cm-diagnostic-controls": {
191
+ marginTop: "4px",
192
+ display: "flex",
193
+ gap: "6px",
194
+ alignItems: "center",
195
+ },
196
+ ".cm-diagnostic-label": {
197
+ whiteSpace: "nowrap",
198
+ fontWeight: "500",
199
+ minWidth: "64px",
200
+ },
201
+ ".cm-diagnostic-textarea": {
202
+ flex: "1",
203
+ padding: "3px 6px",
204
+ border: "1px solid",
205
+ borderRadius: "3px",
206
+ fontSize: "12px",
207
+ fontFamily: "'JetBrains Mono', monospace",
208
+ resize: "none",
209
+ overflow: "auto",
210
+ boxSizing: "border-box",
211
+ },
212
+ ".cm-diagnostic-textarea[readonly]": {
213
+ cursor: "default",
214
+ },
215
+ ".cm-diagnostic-input": {
216
+ padding: "3px 6px",
217
+ border: "1px solid",
218
+ borderRadius: "3px",
219
+ fontSize: "12px",
220
+ fontFamily: "'JetBrains Mono', monospace",
221
+ boxSizing: "border-box",
222
+ width: "100%",
223
+ },
224
+ ".cm-diagnostic-button": {
225
+ padding: "3px 12px",
226
+ border: "1px solid",
227
+ borderRadius: "3px",
228
+ fontSize: "12px",
229
+ fontFamily: "'JetBrains Mono', monospace",
230
+ cursor: "pointer",
231
+ boxSizing: "border-box",
232
+ },
233
+ ".cm-diagnostic-button:active": {
234
+ transform: "translateY(1px)",
235
+ },
236
+ ".cm-diagnostic-error-message": {
237
+ fontSize: "11px",
238
+ marginTop: "2px",
239
+ fontWeight: "500",
240
+ },
241
+ ".cm-timestamp-field": {
242
+ display: "flex",
243
+ flexDirection: "column",
244
+ gap: "2px",
245
+ flex: "1",
246
+ },
247
+ ".cm-tooltip-lint .cm-diagnostic": {
248
+ padding: "0",
249
+ borderTop: "none",
250
+ borderLeft: "none",
251
+ },
252
+ ".cm-tooltip-lint .cm-diagnostic-error": {
253
+ borderLeft: "none",
254
+ fontWeight: "bold",
255
+ },
256
+ ".cm-tooltip-lint .cm-diagnostic-info": {
257
+ borderLeft: "none",
258
+ },
259
+ ".diagnostic-row + .diagnostic-row": {
260
+ marginTop: "8px",
261
+ paddingTop: "8px",
262
+ borderTop: "1px solid",
263
+ },
264
+ }),
265
+ ],
266
+ });
267
+ }
268
+
269
+ export interface CustomTheme {
270
+ backgroundColor: string;
271
+ lighterBgColor: string;
272
+ borderColor: string;
273
+ foregroundColor: string;
274
+ accentColor: string;
275
+ errorColor: string;
276
+ selectionColor: string;
277
+ themeExtension?: Extension;
278
+ }
@@ -0,0 +1,232 @@
1
+ import { Extension } from "@codemirror/state";
2
+ import { EditorView, keymap } from "@codemirror/view";
3
+ import { makeJsonTemplate } from "../json/to_json";
4
+ import {
5
+ ArrayTypeSignature,
6
+ JsonValue,
7
+ RecordDefinition,
8
+ RecordTypeSignature,
9
+ TypeDefinition,
10
+ } from "../json/types";
11
+ import { ensureJsonState } from "./json_state";
12
+
13
+ /**
14
+ * Triggered when the Enter key is pressed between 2 consecutive curly braces or
15
+ * square brackets.
16
+ */
17
+ export function enterKeyHandler(schema: TypeDefinition): Extension {
18
+ return keymap.of([
19
+ {
20
+ key: "Enter",
21
+ run: (view: EditorView): boolean => {
22
+ const { state } = view;
23
+ const { selection } = state;
24
+ const { main } = selection;
25
+
26
+ // Get the current cursor position
27
+ const cursorPos = main.head;
28
+
29
+ const textAround = state.doc.sliceString(cursorPos - 1, cursorPos + 1);
30
+ if (textAround !== "{}" && textAround !== "[]") {
31
+ return false; // Allow default Enter behavior
32
+ }
33
+
34
+ // Ensure JSON state is up-to-date
35
+ const jsonState = ensureJsonState(view, schema);
36
+ const parseResult = jsonState.parseResult;
37
+ if (!parseResult.value) {
38
+ return false;
39
+ }
40
+ const jsonValue: JsonValue = parseResult.value;
41
+
42
+ const idToRecordDef: { [id: string]: RecordDefinition } = {};
43
+ for (const record of schema.records) {
44
+ idToRecordDef[record.id] = record;
45
+ }
46
+ const type = findTypeByLeftBracketPos(
47
+ jsonValue,
48
+ cursorPos - 1,
49
+ idToRecordDef,
50
+ );
51
+ if (type === null) {
52
+ return false;
53
+ }
54
+
55
+ let codeToInsert: string;
56
+ if (type.kind === "array") {
57
+ codeToInsert = JSON.stringify(
58
+ makeJsonTemplate(type.value.item, idToRecordDef),
59
+ null,
60
+ 2,
61
+ );
62
+ } else if (type.kind === "record") {
63
+ const recordDef = idToRecordDef[type.value];
64
+ if (recordDef.kind === "struct") {
65
+ codeToInsert = JSON.stringify(
66
+ makeJsonTemplate(type, idToRecordDef),
67
+ null,
68
+ 2,
69
+ );
70
+ if (codeToInsert === "{}") {
71
+ return false;
72
+ }
73
+ // Remove the curly brackets at the start and end of the string and
74
+ // unindent each line.
75
+ codeToInsert = codeToInsert
76
+ .split("\n")
77
+ .slice(1, -1)
78
+ .map((line) => line.substring(2))
79
+ .join("\n");
80
+ } else {
81
+ // Enum
82
+ codeToInsert = ['"kind": "",', '"value": {}'].join("\n");
83
+ }
84
+ } else if (type.kind === "timestamp") {
85
+ codeToInsert =
86
+ '"unix_millis": 0,\n"formatted": "1970-01-01T00:00:00.000Z"';
87
+ } else {
88
+ const _: never = type;
89
+ throw new Error(_);
90
+ }
91
+
92
+ // Indent the code to insert based on the current line's indentation
93
+ const lineIndentation = getLineIndentation(
94
+ state.doc.toString(),
95
+ cursorPos - 1,
96
+ );
97
+ codeToInsert = indent(codeToInsert, " " + lineIndentation);
98
+
99
+ codeToInsert = `\n${codeToInsert}\n${lineIndentation}`;
100
+
101
+ // Apply the transaction
102
+ view.dispatch({
103
+ changes: {
104
+ from: cursorPos,
105
+ to: cursorPos,
106
+ insert: codeToInsert,
107
+ },
108
+ // Move cursor to the end of the inserted text
109
+ selection: { anchor: cursorPos + codeToInsert.length },
110
+ });
111
+
112
+ return true; // Prevent default Enter behavior
113
+ },
114
+ },
115
+ ]);
116
+ }
117
+
118
+ function findTypeByLeftBracketPos(
119
+ jsonValue: JsonValue,
120
+ leftBracketPos: number,
121
+ idToRecordDef: { [id: string]: RecordDefinition },
122
+ ): ArrayTypeSignature | RecordTypeSignature | { kind: "timestamp" } | null {
123
+ let { expectedType } = jsonValue;
124
+ if (!expectedType) {
125
+ return null;
126
+ }
127
+ if (expectedType.kind === "optional") {
128
+ expectedType = expectedType.value;
129
+ }
130
+ switch (jsonValue.kind) {
131
+ case "array": {
132
+ if (expectedType.kind !== "array") {
133
+ return null;
134
+ }
135
+ if (leftBracketPos === jsonValue.firstToken.start) {
136
+ // We have a match
137
+ return expectedType;
138
+ }
139
+ // If no match, we can continue searching in the elements.
140
+ for (const element of jsonValue.values) {
141
+ if (leftBracketPos < element.firstToken.start) {
142
+ return null; // No need to continue (optimization)
143
+ }
144
+ const maybeResult = findTypeByLeftBracketPos(
145
+ element,
146
+ leftBracketPos,
147
+ idToRecordDef,
148
+ );
149
+ if (maybeResult) {
150
+ return maybeResult;
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ case "object": {
156
+ if (
157
+ expectedType.kind === "primitive" &&
158
+ expectedType.value === "timestamp" &&
159
+ leftBracketPos === jsonValue.firstToken.start
160
+ ) {
161
+ return { kind: "timestamp" };
162
+ }
163
+ if (expectedType.kind !== "record") {
164
+ return null;
165
+ }
166
+ if (leftBracketPos === jsonValue.firstToken.start) {
167
+ // We have a match
168
+ return expectedType;
169
+ }
170
+ // If no match, we can continue searching in the values.
171
+ const recordDef = idToRecordDef[expectedType.value];
172
+ if (recordDef.kind === "struct") {
173
+ for (const keyValue of Object.values(jsonValue.keyValues)) {
174
+ const maybeResult = findTypeByLeftBracketPos(
175
+ keyValue.value,
176
+ leftBracketPos,
177
+ idToRecordDef,
178
+ );
179
+ if (maybeResult) {
180
+ return maybeResult;
181
+ }
182
+ }
183
+ } else {
184
+ const valueKv = jsonValue.keyValues["value"];
185
+ if (valueKv) {
186
+ const maybeResult = findTypeByLeftBracketPos(
187
+ valueKv.value,
188
+ leftBracketPos,
189
+ idToRecordDef,
190
+ );
191
+ if (maybeResult) {
192
+ return maybeResult;
193
+ }
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ case "literal": {
199
+ return null;
200
+ }
201
+ }
202
+ }
203
+
204
+ /** Returns the indentation at the line containing `position`. */
205
+ function getLineIndentation(text: string, position: number): string {
206
+ if (position < 0 || position >= text.length) {
207
+ return "";
208
+ }
209
+
210
+ // Find the start of the line containing the position
211
+ let lineStart = position;
212
+ while (lineStart > 0 && text[lineStart - 1] !== "\n") {
213
+ lineStart--;
214
+ }
215
+
216
+ // Extract indentation (spaces at the beginning of the line)
217
+ let indentation = "";
218
+ let i = lineStart;
219
+ while (i < text.length && text[i] === " ") {
220
+ indentation += " ";
221
+ i++;
222
+ }
223
+
224
+ return indentation;
225
+ }
226
+
227
+ function indent(text: string, indentation: string): string {
228
+ return text
229
+ .split("\n")
230
+ .map((line) => indentation + line)
231
+ .join("\n");
232
+ }