correctly 0.0.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.
@@ -0,0 +1,168 @@
1
+ import {
2
+ findNodeAtLocation,
3
+ getNodePath,
4
+ getNodeValue,
5
+ parseTree,
6
+ printParseErrorCode,
7
+ type Node,
8
+ type ParseError,
9
+ } from "jsonc-parser"
10
+ import type { Diagnostic, Mode, Position } from "./types.ts"
11
+
12
+ export type SourceSpan = { offset: number; length: number }
13
+ export type ParsedDocument = {
14
+ value: unknown
15
+ diagnostics: Diagnostic[]
16
+ locate: (pointer: string, key?: boolean) => SourceSpan | undefined
17
+ }
18
+ export type ParsedJsonDocument = ParsedDocument & { root: Node | undefined }
19
+
20
+ export function pointer(parts: readonly (string | number)[]): string {
21
+ return parts
22
+ .map((p) => `/${String(p).replace(/~/g, "~0").replace(/\//g, "~1")}`)
23
+ .join("")
24
+ }
25
+
26
+ export function positionAt(text: string, offset: number): Position {
27
+ const end = Math.min(Math.max(0, offset), text.length)
28
+ let line = 0
29
+ let start = 0
30
+ for (let i = 0; i < end; i++) {
31
+ if (text[i] === "\r") {
32
+ if (text[i + 1] === "\n" && i + 1 < end) i++
33
+ line++
34
+ start = i + 1
35
+ } else if (text[i] === "\n") {
36
+ line++
37
+ start = i + 1
38
+ }
39
+ }
40
+ return { line, character: end - start }
41
+ }
42
+
43
+ export function diagnostic(
44
+ text: string,
45
+ code: string,
46
+ message: string,
47
+ jsonPointer = "",
48
+ offset = 0,
49
+ length = 1,
50
+ ): Diagnostic {
51
+ const start = Math.min(offset, text.length)
52
+ const size = Math.min(Math.max(1, length), text.length - start)
53
+ return {
54
+ code,
55
+ message,
56
+ pointer: jsonPointer,
57
+ offset: start,
58
+ length: size,
59
+ range: {
60
+ start: positionAt(text, start),
61
+ end: positionAt(text, start + size),
62
+ },
63
+ }
64
+ }
65
+
66
+ export function parseDocument(text: string, mode: Mode): ParsedJsonDocument {
67
+ const errors: ParseError[] = []
68
+ const root = parseTree(text, errors, {
69
+ disallowComments: mode === "json",
70
+ allowTrailingComma: mode === "jsonc",
71
+ allowEmptyContent: false,
72
+ })
73
+ const diagnostics = errors.map((e) =>
74
+ diagnostic(
75
+ text,
76
+ "syntax",
77
+ printParseErrorCode(e.error),
78
+ "",
79
+ e.offset,
80
+ e.length,
81
+ ),
82
+ )
83
+ function visit(node: Node) {
84
+ if (node.type === "object") {
85
+ const keys = new Set<string>()
86
+ for (const property of node.children ?? []) {
87
+ const key = property.children?.[0]
88
+ if (!key || typeof key.value !== "string") continue
89
+ if (keys.has(key.value)) {
90
+ diagnostics.push(
91
+ diagnostic(
92
+ text,
93
+ "duplicate-key",
94
+ `Duplicate key ${JSON.stringify(key.value)}`,
95
+ pointer([...getNodePath(node), key.value]),
96
+ key.offset,
97
+ key.length,
98
+ ),
99
+ )
100
+ }
101
+ keys.add(key.value)
102
+ }
103
+ }
104
+ if (node.type === "number" && !Number.isFinite(node.value)) {
105
+ diagnostics.push(
106
+ diagnostic(
107
+ text,
108
+ "syntax",
109
+ "Number exceeds the supported finite range",
110
+ pointer(getNodePath(node)),
111
+ node.offset,
112
+ node.length,
113
+ ),
114
+ )
115
+ }
116
+ for (const child of node.children ?? []) visit(child)
117
+ }
118
+ if (root) visit(root)
119
+ // jsonc-parser's object values have null prototypes. Ajv's deep equality
120
+ // expects ordinary JSON objects; Object.fromEntries safely preserves __proto__.
121
+ function value(node: Node): unknown {
122
+ if (node.type === "object")
123
+ return Object.fromEntries(
124
+ (node.children ?? []).flatMap((property) => {
125
+ const key = property.children?.[0]
126
+ const child = property.children?.[1]
127
+ return key && child ? [[String(key.value), value(child)]] : []
128
+ }),
129
+ )
130
+ if (node.type === "array") return (node.children ?? []).map(value)
131
+ return getNodeValue(node) as unknown
132
+ }
133
+ return {
134
+ root,
135
+ value: root ? value(root) : undefined,
136
+ diagnostics,
137
+ locate: (jsonPointer, key = false) => {
138
+ const node = locate(root, jsonPointer, key)
139
+ return node ? { offset: node.offset, length: node.length } : undefined
140
+ },
141
+ }
142
+ }
143
+
144
+ export function locate(
145
+ root: Node | undefined,
146
+ jsonPointer: string,
147
+ key = false,
148
+ ): Node | undefined {
149
+ if (!root) return undefined
150
+ const parts =
151
+ jsonPointer === ""
152
+ ? []
153
+ : jsonPointer
154
+ .slice(1)
155
+ .split("/")
156
+ .map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"))
157
+ let current = root
158
+ for (const part of parts) {
159
+ const next = findNodeAtLocation(current, [
160
+ current.type === "array" ? Number(part) : part,
161
+ ])
162
+ if (!next) break
163
+ current = next
164
+ }
165
+ return key && current.parent?.type === "property"
166
+ ? (current.parent.children?.[0] ?? current)
167
+ : current
168
+ }
@@ -0,0 +1,379 @@
1
+ import { createHash, randomUUID } from "node:crypto"
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises"
3
+ import path from "node:path"
4
+ import { fileURLToPath } from "node:url"
5
+ import type { AnySchema, SchemaObject } from "ajv"
6
+ import { parseDocument } from "./parse.ts"
7
+ import { readText, type Project, type ReadText } from "./config.ts"
8
+ import { CorrectlyError } from "./types.ts"
9
+
10
+ export type Dialect = "draft7" | "2020-12"
11
+ export const DIALECTS = {
12
+ draft7: "http://json-schema.org/draft-07/schema#",
13
+ "2020-12": "https://json-schema.org/draft/2020-12/schema",
14
+ } as const
15
+ const VOCABULARIES = new Set(
16
+ [
17
+ "core",
18
+ "applicator",
19
+ "unevaluated",
20
+ "validation",
21
+ "meta-data",
22
+ "format-annotation",
23
+ "content",
24
+ ].map((name) => `https://json-schema.org/draft/2020-12/vocab/${name}`),
25
+ )
26
+ const SINGLE_SCHEMAS = [
27
+ "additionalItems",
28
+ "additionalProperties",
29
+ "contains",
30
+ "propertyNames",
31
+ "not",
32
+ "if",
33
+ "then",
34
+ "else",
35
+ "unevaluatedProperties",
36
+ "unevaluatedItems",
37
+ "contentSchema",
38
+ ]
39
+ const MAP_SCHEMAS = [
40
+ "properties",
41
+ "patternProperties",
42
+ "definitions",
43
+ "$defs",
44
+ "dependentSchemas",
45
+ "dependencies",
46
+ ]
47
+ const ARRAY_SCHEMAS = ["allOf", "anyOf", "oneOf", "prefixItems"]
48
+
49
+ function isObject(value: unknown): value is SchemaObject {
50
+ return typeof value === "object" && value !== null && !Array.isArray(value)
51
+ }
52
+ function dialect(value: unknown, inherited: Dialect): Dialect {
53
+ if (value === undefined) return inherited
54
+ if (typeof value !== "string")
55
+ throw new CorrectlyError("schema", "Schema $schema must be a string")
56
+ const id = value.replace(/#$/, "")
57
+ if (
58
+ id === "http://json-schema.org/draft-07/schema" ||
59
+ id === "https://json-schema.org/draft-07/schema"
60
+ )
61
+ return "draft7"
62
+ if (id === DIALECTS["2020-12"]) return "2020-12"
63
+ throw new CorrectlyError(
64
+ "unsupported-dialect",
65
+ `Unsupported schema dialect: ${value}; supported: draft 7, draft 2020-12`,
66
+ )
67
+ }
68
+
69
+ export type SchemaResource = {
70
+ uri: string
71
+ schema: AnySchema
72
+ dialect: Dialect
73
+ }
74
+ type CacheEntry = {
75
+ uri: string
76
+ text: string
77
+ etag?: string
78
+ lastModified?: string
79
+ }
80
+ export type StoreOptions = {
81
+ offline?: boolean
82
+ read?: ReadText
83
+ fetch?: typeof fetch
84
+ signal?: AbortSignal
85
+ }
86
+
87
+ export class SchemaStore {
88
+ private readonly options: StoreOptions
89
+ readonly resources = new Map<string, SchemaResource>()
90
+ private readonly pending = new Map<string, Promise<SchemaResource>>()
91
+ private readonly cacheDir: string
92
+ private readonly offline: boolean
93
+ private readonly timeoutMs: number
94
+ private readonly maxBytes: number
95
+ private readonly maxRequests: number
96
+ private readonly read: ReadText
97
+ private readonly request: typeof fetch
98
+ private requests = 0
99
+ constructor(project: Project, options: StoreOptions = {}) {
100
+ this.options = options
101
+ const remote = project.config.remote
102
+ this.cacheDir = path.resolve(
103
+ project.root,
104
+ remote?.cacheDir ?? ".correctly-cache",
105
+ )
106
+ this.offline = options.offline ?? remote?.offline ?? false
107
+ this.timeoutMs = remote?.timeoutMs ?? 5000
108
+ this.maxBytes = remote?.maxBytes ?? 2 * 1024 * 1024
109
+ this.maxRequests = remote?.maxRequests ?? 64
110
+ this.read = options.read ?? readText
111
+ this.request = options.fetch ?? fetch
112
+ }
113
+
114
+ async load(
115
+ value: string,
116
+ inherited: Dialect = "draft7",
117
+ ): Promise<SchemaResource> {
118
+ this.options.signal?.throwIfAborted()
119
+ const url = new URL(value)
120
+ url.hash = ""
121
+ const uri = url.href
122
+ const known = this.resources.get(uri)
123
+ if (known) return known
124
+ let promise = this.pending.get(uri)
125
+ if (!promise) {
126
+ promise = this.loadResource(uri, inherited)
127
+ this.pending.set(uri, promise)
128
+ }
129
+ return promise
130
+ }
131
+
132
+ private async loadResource(
133
+ uri: string,
134
+ inherited: Dialect,
135
+ ): Promise<SchemaResource> {
136
+ if (++this.requests > this.maxRequests)
137
+ throw new CorrectlyError(
138
+ "schema-limit",
139
+ `Schema resource limit (${this.maxRequests}) exceeded`,
140
+ )
141
+ let text: string
142
+ const url = new URL(uri)
143
+ try {
144
+ if (url.protocol === "file:") text = await this.read(uri)
145
+ else if (url.protocol === "https:" || url.protocol === "http:")
146
+ text = await this.remote(uri)
147
+ else
148
+ throw new CorrectlyError(
149
+ "schema",
150
+ `Unsupported schema protocol: ${url.protocol}`,
151
+ )
152
+ } catch (error) {
153
+ if (error instanceof CorrectlyError) throw error
154
+ throw new CorrectlyError(
155
+ "schema-load",
156
+ `Cannot load schema ${uri}: ${error instanceof Error ? error.message : String(error)}`,
157
+ )
158
+ }
159
+ this.options.signal?.throwIfAborted()
160
+ if (Buffer.byteLength(text) > this.maxBytes)
161
+ throw new CorrectlyError(
162
+ "schema-limit",
163
+ `Schema ${uri} exceeds ${this.maxBytes} bytes`,
164
+ )
165
+ const parsed = parseDocument(text, "json")
166
+ if (parsed.diagnostics.length) {
167
+ const d = parsed.diagnostics[0]!
168
+ throw new CorrectlyError(
169
+ "schema",
170
+ `Invalid schema JSON at ${uri}:${d.range.start.line + 1}:${d.range.start.character + 1}: ${d.message}`,
171
+ )
172
+ }
173
+ if (typeof parsed.value !== "boolean" && !isObject(parsed.value))
174
+ throw new CorrectlyError(
175
+ "schema",
176
+ `Schema must be an object or boolean: ${uri}`,
177
+ )
178
+ const rootDialect = dialect(
179
+ isObject(parsed.value) ? parsed.value.$schema : undefined,
180
+ inherited,
181
+ )
182
+ const schema = parsed.value as AnySchema
183
+ const resource: SchemaResource = { uri, schema, dialect: rootDialect }
184
+ // Normalize schema identifiers only, never document data or annotation values.
185
+ this.normalize(schema, uri, rootDialect, resource)
186
+ if (isObject(schema) && schema.$id === undefined) schema.$id = uri
187
+ this.register(uri, resource)
188
+ return resource
189
+ }
190
+
191
+ private register(uri: string, resource: SchemaResource) {
192
+ const previous = this.resources.get(uri)
193
+ if (previous && previous.schema !== resource.schema)
194
+ throw new CorrectlyError("schema", `Duplicate schema identifier: ${uri}`)
195
+ this.resources.set(uri, resource)
196
+ }
197
+
198
+ private normalize(
199
+ schema: unknown,
200
+ base: string,
201
+ expected: Dialect,
202
+ root: SchemaResource,
203
+ ) {
204
+ if (!isObject(schema)) return
205
+ const ownDialect = dialect(schema.$schema, expected)
206
+ if (ownDialect !== expected)
207
+ throw new CorrectlyError(
208
+ "unsupported-dialect",
209
+ `Mixed schema dialects at ${base} are not supported`,
210
+ )
211
+ if (schema.$schema !== undefined) schema.$schema = DIALECTS[ownDialect]
212
+ if (schema.$async !== undefined)
213
+ throw new CorrectlyError(
214
+ "schema",
215
+ `Asynchronous data validation is not supported: ${base}`,
216
+ )
217
+ if (schema.$vocabulary !== undefined) {
218
+ if (!isObject(schema.$vocabulary))
219
+ throw new CorrectlyError(
220
+ "schema",
221
+ `$vocabulary must be an object: ${base}`,
222
+ )
223
+ for (const [vocabulary, required] of Object.entries(schema.$vocabulary)) {
224
+ if (typeof required !== "boolean")
225
+ throw new CorrectlyError(
226
+ "schema",
227
+ `$vocabulary values must be boolean: ${base}`,
228
+ )
229
+ if (required && !VOCABULARIES.has(vocabulary))
230
+ throw new CorrectlyError(
231
+ "unsupported-vocabulary",
232
+ `Unsupported required vocabulary: ${vocabulary}`,
233
+ )
234
+ }
235
+ }
236
+ let scope = base
237
+ if (schema.$id !== undefined) {
238
+ if (typeof schema.$id !== "string")
239
+ throw new CorrectlyError("schema", `$id must be a string: ${base}`)
240
+ scope = new URL(schema.$id, base).href
241
+ schema.$id = scope
242
+ this.register(scope.replace(/#$/, ""), { ...root, schema })
243
+ }
244
+ for (const anchor of [schema.$anchor, schema.$dynamicAnchor]) {
245
+ if (typeof anchor === "string")
246
+ this.register(new URL(`#${anchor}`, scope).href, { ...root, schema })
247
+ }
248
+ for (const key of SINGLE_SCHEMAS)
249
+ this.normalize(schema[key], scope, expected, root)
250
+ for (const key of MAP_SCHEMAS) {
251
+ if (isObject(schema[key]))
252
+ for (const child of Object.values(
253
+ schema[key] as Record<string, unknown>,
254
+ ))
255
+ this.normalize(child, scope, expected, root)
256
+ }
257
+ for (const key of [...ARRAY_SCHEMAS, "items"]) {
258
+ const children: unknown = schema[key]
259
+ if (Array.isArray(children))
260
+ for (const child of children)
261
+ this.normalize(child, scope, expected, root)
262
+ else if (key === "items") this.normalize(children, scope, expected, root)
263
+ }
264
+ }
265
+
266
+ private async remote(uri: string): Promise<string> {
267
+ const cacheFile = path.join(
268
+ this.cacheDir,
269
+ `${createHash("sha256").update(uri).digest("hex")}.json`,
270
+ )
271
+ let cached: CacheEntry | undefined
272
+ try {
273
+ const raw = await readFile(cacheFile, "utf8")
274
+ const entry: unknown = JSON.parse(raw)
275
+ if (
276
+ !isObject(entry) ||
277
+ entry.uri !== uri ||
278
+ typeof entry.text !== "string"
279
+ )
280
+ throw new Error("invalid cache entry")
281
+ cached = entry as CacheEntry
282
+ } catch (error) {
283
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT")
284
+ throw new CorrectlyError(
285
+ "schema-cache",
286
+ `Cannot read schema cache ${cacheFile}: ${String(error)}`,
287
+ )
288
+ }
289
+ if (this.offline) {
290
+ if (!cached)
291
+ throw new CorrectlyError(
292
+ "offline-miss",
293
+ `Schema is not cached in offline mode: ${uri}`,
294
+ )
295
+ return cached.text
296
+ }
297
+ const timeout = AbortSignal.timeout(this.timeoutMs)
298
+ const signal = this.options.signal
299
+ ? AbortSignal.any([this.options.signal, timeout])
300
+ : timeout
301
+ const headers: Record<string, string> = {}
302
+ if (cached?.etag) headers["If-None-Match"] = cached.etag
303
+ if (cached?.lastModified) headers["If-Modified-Since"] = cached.lastModified
304
+ let current = uri
305
+ for (let redirects = 0; redirects <= 5; redirects++) {
306
+ const response = await this.request(current, {
307
+ signal,
308
+ headers,
309
+ redirect: "manual",
310
+ })
311
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
312
+ await response.body?.cancel()
313
+ const location = response.headers.get("location")
314
+ if (!location)
315
+ throw new CorrectlyError(
316
+ "schema-load",
317
+ `Redirect without location: ${current}`,
318
+ )
319
+ current = new URL(location, current).href
320
+ if (!["http:", "https:"].includes(new URL(current).protocol))
321
+ throw new CorrectlyError(
322
+ "schema-load",
323
+ `Unsupported redirect: ${current}`,
324
+ )
325
+ continue
326
+ }
327
+ if (response.status === 304 && cached) {
328
+ await response.body?.cancel()
329
+ return cached.text
330
+ }
331
+ if (!response.ok || !response.body) {
332
+ await response.body?.cancel()
333
+ throw new CorrectlyError(
334
+ "schema-load",
335
+ `HTTP ${response.status} loading schema ${uri}`,
336
+ )
337
+ }
338
+ const reader = response.body.getReader()
339
+ const chunks: Uint8Array[] = []
340
+ let size = 0
341
+ try {
342
+ while (true) {
343
+ const { value, done } = await reader.read()
344
+ if (done) break
345
+ size += value.byteLength
346
+ if (size > this.maxBytes)
347
+ throw new CorrectlyError(
348
+ "schema-limit",
349
+ `Schema ${uri} exceeds ${this.maxBytes} bytes`,
350
+ )
351
+ chunks.push(value)
352
+ }
353
+ } finally {
354
+ await reader.cancel()
355
+ }
356
+ const text = Buffer.concat(chunks).toString("utf8")
357
+ const entry: CacheEntry = { uri, text }
358
+ const etag = response.headers.get("etag")
359
+ const lastModified = response.headers.get("last-modified")
360
+ if (etag) entry.etag = etag
361
+ if (lastModified) entry.lastModified = lastModified
362
+ signal.throwIfAborted()
363
+ await mkdir(this.cacheDir, { recursive: true })
364
+ const temporary = `${cacheFile}.${randomUUID()}.tmp`
365
+ await writeFile(temporary, JSON.stringify(entry))
366
+ await rename(temporary, cacheFile)
367
+ return text
368
+ }
369
+ throw new CorrectlyError(
370
+ "schema-limit",
371
+ `Too many redirects loading schema ${uri}`,
372
+ )
373
+ }
374
+ }
375
+
376
+ export function localSchemaPath(uri: string): string | undefined {
377
+ const url = new URL(uri)
378
+ return url.protocol === "file:" ? fileURLToPath(url) : undefined
379
+ }
@@ -0,0 +1,56 @@
1
+ export type Mode = "json" | "jsonc"
2
+ export type Position = { line: number; character: number }
3
+ export type Range = { start: Position; end: Position }
4
+ export type Diagnostic = {
5
+ code: string
6
+ message: string
7
+ pointer: string
8
+ offset: number
9
+ length: number
10
+ range: Range
11
+ context?: { parent: number; label: string }
12
+ }
13
+ export type Failure = { code: string; message: string; file?: string }
14
+ export class CorrectlyError extends Error {
15
+ readonly code: string
16
+ constructor(code: string, message: string) {
17
+ super(message)
18
+ this.name = "CorrectlyError"
19
+ this.code = code
20
+ }
21
+ }
22
+ export function failure(error: unknown, file?: string): Failure {
23
+ return {
24
+ code: error instanceof CorrectlyError ? error.code : "execution",
25
+ message: error instanceof Error ? error.message : String(error),
26
+ ...(file === undefined ? {} : { file }),
27
+ }
28
+ }
29
+ export type Association = {
30
+ index: number
31
+ name: string
32
+ schema: string | null
33
+ mode: Mode
34
+ }
35
+ export type FileResult = {
36
+ file: string
37
+ mode: Mode
38
+ coverage: "schema" | "syntax-only" | "excluded"
39
+ association: Association | null
40
+ diagnostics: Diagnostic[]
41
+ failures: Failure[]
42
+ }
43
+ export type Report = {
44
+ reportVersion: 1
45
+ config: string | null
46
+ files: FileResult[]
47
+ failures: Failure[]
48
+ summary: {
49
+ checked: number
50
+ schemaCovered: number
51
+ syntaxOnly: number
52
+ invalid: number
53
+ failures: number
54
+ }
55
+ exitCode: 0 | 1 | 2
56
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ getLanguageService,
3
+ type JSONDocument,
4
+ type LanguageService,
5
+ type Position,
6
+ } from "vscode-json-languageservice"
7
+ import type { TextDocument } from "vscode-languageserver-textdocument"
8
+ import { fileURLToPath } from "node:url"
9
+ import { associationFor, isIncluded } from "../core/config.ts"
10
+ import type { Engine } from "../core/engine.ts"
11
+
12
+ // Only the schema-selection accessor is hidden. The original AST (including
13
+ // $schema as ordinary data) is retained by every bound traversal method.
14
+ export function externalSelection(document: JSONDocument): JSONDocument {
15
+ return new Proxy(document, {
16
+ get(target, key) {
17
+ if (key === "root") return undefined
18
+ const value: unknown = Reflect.get(target, key)
19
+ return typeof value === "function" ? value.bind(target) : value
20
+ },
21
+ })
22
+ }
23
+
24
+ export class Hints {
25
+ private readonly services = new WeakMap<
26
+ Engine,
27
+ Map<string, LanguageService>
28
+ >()
29
+ private async service(
30
+ engine: Engine,
31
+ document: TextDocument,
32
+ ): Promise<LanguageService | undefined> {
33
+ const file = fileURLToPath(document.uri)
34
+ if (!isIncluded(engine.project, file)) return undefined
35
+ const association = associationFor(engine.project, file)
36
+ if (!association?.schema) return undefined
37
+ await engine.validator(association.schema)
38
+ let services = this.services.get(engine)
39
+ if (!services) {
40
+ services = new Map()
41
+ this.services.set(engine, services)
42
+ }
43
+ let service = services.get(association.schema)
44
+ if (!service) {
45
+ service = getLanguageService({
46
+ schemaRequestService: async (uri) =>
47
+ JSON.stringify((await engine.store.load(uri)).schema),
48
+ workspaceContext: {
49
+ resolveRelativePath: (relative, resource) =>
50
+ new URL(relative, resource).href,
51
+ },
52
+ })
53
+ // A per-association service prevents overlapping rules being combined.
54
+ service.configure({
55
+ validate: false,
56
+ schemas: [{ uri: association.schema, fileMatch: ["*"] }],
57
+ })
58
+ services.set(association.schema, service)
59
+ }
60
+ return service
61
+ }
62
+ async complete(engine: Engine, document: TextDocument, position: Position) {
63
+ const service = await this.service(engine, document)
64
+ return service
65
+ ? await service.doComplete(
66
+ document,
67
+ position,
68
+ externalSelection(service.parseJSONDocument(document)),
69
+ )
70
+ : { isIncomplete: false, items: [] }
71
+ }
72
+ async hover(engine: Engine, document: TextDocument, position: Position) {
73
+ const service = await this.service(engine, document)
74
+ return service
75
+ ? await service.doHover(
76
+ document,
77
+ position,
78
+ externalSelection(service.parseJSONDocument(document)),
79
+ )
80
+ : null
81
+ }
82
+ }