@soda-gql/lsp 0.14.1 → 0.14.2
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.
- package/README.md +45 -0
- package/dist/bin.cjs +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/index.cjs +298 -2
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +42 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +44 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +297 -2
- package/dist/index.mjs.map +1 -0
- package/dist/{server-DsJ1bZ7i.cjs → server-C1MSX490.cjs} +1101 -410
- package/dist/server-C1MSX490.cjs.map +1 -0
- package/dist/{server-CqOUHwDk.mjs → server-wPCHK04O.mjs} +1087 -414
- package/dist/server-wPCHK04O.mjs.map +1 -0
- package/package.json +6 -6
- package/dist/server-CqOUHwDk.mjs.map +0 -1
- package/dist/server-DsJ1bZ7i.cjs.map +0 -1
|
@@ -1,19 +1,154 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { createSwcSpanConverter } from "@soda-gql/common";
|
|
4
|
-
import { formatTemplatesInSource, walkAndExtract } from "@soda-gql/common/template-extraction";
|
|
5
|
-
import { TypeInfo, buildASTSchema, concatAST, getNamedType, isTypeDefinitionNode, parse, print, visit, visitWithTypeInfo } from "graphql";
|
|
6
2
|
import { readFileSync } from "node:fs";
|
|
7
3
|
import { dirname, resolve, sep } from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
import { err, ok } from "neverthrow";
|
|
4
|
+
import { createGraphqlSystemIdentifyHelper } from "@soda-gql/builder";
|
|
10
5
|
import { findAllConfigFiles, findConfigFile, loadConfig } from "@soda-gql/config";
|
|
6
|
+
import { GraphQLUnionType, Kind, TypeInfo, buildASTSchema, concatAST, getNamedType, isObjectType, isTypeDefinitionNode, parse, print, visit, visitWithTypeInfo } from "graphql";
|
|
7
|
+
import { getAutocompleteSuggestions, getContextAtPosition, getDefinitionQueryResultForField, getDefinitionQueryResultForFragmentSpread, getDefinitionQueryResultForNamedType, getDiagnostics, getHoverInformation, getOutline } from "graphql-language-service";
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
+
import { createSwcSpanConverter } from "@soda-gql/common";
|
|
10
|
+
import { formatTemplatesInSource, walkAndExtract, walkAndExtractFieldTrees } from "@soda-gql/common/template-extraction";
|
|
11
|
+
import { err, ok } from "neverthrow";
|
|
12
|
+
import { hashSchema } from "@soda-gql/tools/codegen";
|
|
11
13
|
import { DidChangeWatchedFilesNotification, FileChangeType, ProposedFeatures, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
|
|
12
14
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
13
|
-
import { createGraphqlSystemIdentifyHelper } from "@soda-gql/builder";
|
|
14
15
|
import ts from "typescript";
|
|
15
|
-
import { getAutocompleteSuggestions, getContextAtPosition, getDefinitionQueryResultForField, getDefinitionQueryResultForFragmentSpread, getDiagnostics, getHoverInformation, getOutline } from "graphql-language-service";
|
|
16
16
|
|
|
17
|
+
//#region packages/lsp/src/field-tree-resolver.ts
|
|
18
|
+
/** Get the root type name for an operation kind from the schema. */
|
|
19
|
+
const getRootTypeName = (schema, kind) => {
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "query": return schema.getQueryType()?.name ?? null;
|
|
22
|
+
case "mutation": return schema.getMutationType()?.name ?? null;
|
|
23
|
+
case "subscription": return schema.getSubscriptionType()?.name ?? null;
|
|
24
|
+
default: return null;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** Classify a GraphQL named type into a kind string. */
|
|
28
|
+
const classifyType = (namedType) => {
|
|
29
|
+
if (isObjectType(namedType)) return "object";
|
|
30
|
+
if (namedType instanceof GraphQLUnionType) return "union";
|
|
31
|
+
if ("getValues" in namedType) return "enum";
|
|
32
|
+
return "scalar";
|
|
33
|
+
};
|
|
34
|
+
/** Resolve a single field from a parent object type. */
|
|
35
|
+
const resolveField = (schema, parentTypeName, fieldName) => {
|
|
36
|
+
const parentType = schema.getType(parentTypeName);
|
|
37
|
+
if (!parentType || !isObjectType(parentType)) return null;
|
|
38
|
+
const fields = parentType.getFields();
|
|
39
|
+
const fieldDef = fields[fieldName];
|
|
40
|
+
if (!fieldDef) return null;
|
|
41
|
+
const namedType = getNamedType(fieldDef.type);
|
|
42
|
+
if (!namedType) return null;
|
|
43
|
+
return {
|
|
44
|
+
fieldDef,
|
|
45
|
+
namedType
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/** Resolve untyped FieldCallNode children into typed nodes. */
|
|
49
|
+
const resolveChildren = (schema, parentTypeName, children) => {
|
|
50
|
+
return children.map((child) => resolveNode(schema, parentTypeName, child));
|
|
51
|
+
};
|
|
52
|
+
/** Resolve a single FieldCallNode into a TypedFieldNode. */
|
|
53
|
+
const resolveNode = (schema, parentTypeName, node) => {
|
|
54
|
+
const resolved = resolveField(schema, parentTypeName, node.fieldName);
|
|
55
|
+
const fieldTypeName = resolved?.namedType.name ?? null;
|
|
56
|
+
const fieldTypeKind = resolved ? classifyType(resolved.namedType) : null;
|
|
57
|
+
let nested = null;
|
|
58
|
+
if (node.nested) {
|
|
59
|
+
if (node.nested.kind === "object" && fieldTypeName) {
|
|
60
|
+
nested = {
|
|
61
|
+
kind: "object",
|
|
62
|
+
span: node.nested.span,
|
|
63
|
+
children: resolveChildren(schema, fieldTypeName, node.nested.children)
|
|
64
|
+
};
|
|
65
|
+
} else if (node.nested.kind === "union") {
|
|
66
|
+
nested = resolveUnionNested(schema, resolved?.namedType ?? null, node.nested);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
fieldName: node.fieldName,
|
|
71
|
+
fieldNameSpan: node.fieldNameSpan,
|
|
72
|
+
callSpan: node.callSpan,
|
|
73
|
+
parentTypeName,
|
|
74
|
+
fieldTypeName,
|
|
75
|
+
fieldTypeKind,
|
|
76
|
+
nested
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
/** Resolve union branches against a union type. */
|
|
80
|
+
const resolveUnionNested = (schema, unionType, nested) => {
|
|
81
|
+
const memberNames = new Set();
|
|
82
|
+
if (unionType instanceof GraphQLUnionType) {
|
|
83
|
+
for (const member of unionType.getTypes()) {
|
|
84
|
+
memberNames.add(member.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const branches = nested.branches.map((branch) => ({
|
|
88
|
+
typeName: branch.typeName,
|
|
89
|
+
typeNameSpan: branch.typeNameSpan,
|
|
90
|
+
branchSpan: branch.branchSpan,
|
|
91
|
+
valid: memberNames.has(branch.typeName),
|
|
92
|
+
children: resolveChildren(schema, branch.typeName, branch.children)
|
|
93
|
+
}));
|
|
94
|
+
return {
|
|
95
|
+
kind: "union",
|
|
96
|
+
span: nested.span,
|
|
97
|
+
branches
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Resolve an ExtractedFieldTree against a GraphQL schema.
|
|
102
|
+
* Returns null if the operation kind has no corresponding root type.
|
|
103
|
+
*/
|
|
104
|
+
const resolveFieldTree = (tree, schema) => {
|
|
105
|
+
const rootTypeName = getRootTypeName(schema, tree.kind);
|
|
106
|
+
if (!rootTypeName) return null;
|
|
107
|
+
return {
|
|
108
|
+
schemaName: tree.schemaName,
|
|
109
|
+
rootTypeName,
|
|
110
|
+
rootSpan: tree.rootSpan,
|
|
111
|
+
children: resolveChildren(schema, rootTypeName, tree.children)
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Find the TypedFieldNode or TypedUnionBranch at a given offset.
|
|
116
|
+
* Searches fieldNameSpan for field matches and typeNameSpan for union member matches.
|
|
117
|
+
*/
|
|
118
|
+
const findNodeAtOffset = (tree, offset) => {
|
|
119
|
+
return findInChildren(tree.children, offset, null);
|
|
120
|
+
};
|
|
121
|
+
const findInChildren = (children, offset, _parentForUnion) => {
|
|
122
|
+
for (const node of children) {
|
|
123
|
+
if (offset >= node.fieldNameSpan.start && offset <= node.fieldNameSpan.end) {
|
|
124
|
+
return {
|
|
125
|
+
kind: "field",
|
|
126
|
+
node
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (node.nested) {
|
|
130
|
+
if (node.nested.kind === "object") {
|
|
131
|
+
const result = findInChildren(node.nested.children, offset, null);
|
|
132
|
+
if (result) return result;
|
|
133
|
+
} else if (node.nested.kind === "union") {
|
|
134
|
+
for (const branch of node.nested.branches) {
|
|
135
|
+
if (offset >= branch.typeNameSpan.start && offset <= branch.typeNameSpan.end) {
|
|
136
|
+
return {
|
|
137
|
+
kind: "unionMember",
|
|
138
|
+
branch,
|
|
139
|
+
parentNode: node
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const result = findInChildren(branch.children, offset, node);
|
|
143
|
+
if (result) return result;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
17
152
|
//#region packages/lsp/src/fragment-args-preprocessor.ts
|
|
18
153
|
/**
|
|
19
154
|
* Find the matching closing parenthesis for a balanced group.
|
|
@@ -171,9 +306,17 @@ const collectGqlIdentifiers = (module, filePath, helper) => {
|
|
|
171
306
|
/**
|
|
172
307
|
* Reconstruct full GraphQL source from an extracted template.
|
|
173
308
|
* Prepends the definition header from curried tag call arguments.
|
|
309
|
+
*
|
|
310
|
+
* For callback-variables templates (source === "callback-variables"), wraps the
|
|
311
|
+
* partial variables string in a dummy operation to produce valid GraphQL that
|
|
312
|
+
* graphql-language-service can parse.
|
|
174
313
|
*/
|
|
175
314
|
const reconstructGraphql = (template) => {
|
|
176
315
|
const content = template.content;
|
|
316
|
+
if (template.source === "callback-variables") {
|
|
317
|
+
const name = template.elementName ?? "__variables__";
|
|
318
|
+
return `${template.kind} ${name} ${content} { __typename }`;
|
|
319
|
+
}
|
|
177
320
|
if (template.elementName) {
|
|
178
321
|
if (template.kind === "fragment" && template.typeName) {
|
|
179
322
|
return `fragment ${template.elementName} on ${template.typeName} ${content}`;
|
|
@@ -182,6 +325,21 @@ const reconstructGraphql = (template) => {
|
|
|
182
325
|
}
|
|
183
326
|
return content;
|
|
184
327
|
};
|
|
328
|
+
/**
|
|
329
|
+
* Compute the length of the synthesized prefix before the template content
|
|
330
|
+
* in the reconstructed GraphQL string.
|
|
331
|
+
*
|
|
332
|
+
* For tagged templates, headerLen = reconstructed.length - content.length (content is at the end).
|
|
333
|
+
* For callback-variables, content is in the MIDDLE (prefix + content + suffix), so we must
|
|
334
|
+
* compute the prefix length explicitly.
|
|
335
|
+
*/
|
|
336
|
+
const computeHeaderLen = (template, reconstructed) => {
|
|
337
|
+
if (template.source === "callback-variables") {
|
|
338
|
+
const name = template.elementName ?? "__variables__";
|
|
339
|
+
return `${template.kind} ${name} `.length;
|
|
340
|
+
}
|
|
341
|
+
return reconstructed.length - template.content.length;
|
|
342
|
+
};
|
|
185
343
|
const indexFragments = (uri, templates, source) => {
|
|
186
344
|
const fragments = [];
|
|
187
345
|
for (const template of templates) {
|
|
@@ -189,7 +347,7 @@ const indexFragments = (uri, templates, source) => {
|
|
|
189
347
|
continue;
|
|
190
348
|
}
|
|
191
349
|
const reconstructed = reconstructGraphql(template);
|
|
192
|
-
const headerLen = reconstructed
|
|
350
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
193
351
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
194
352
|
try {
|
|
195
353
|
const ast = parse(preprocessed, { noLocation: false });
|
|
@@ -263,33 +421,45 @@ const createDocumentManager = (helper, swcOptions) => {
|
|
|
263
421
|
};
|
|
264
422
|
const cache = new Map();
|
|
265
423
|
const fragmentIndex = new Map();
|
|
266
|
-
const
|
|
424
|
+
const extractAll = (uri, source) => {
|
|
267
425
|
const isTsx = uri.endsWith(".tsx");
|
|
268
426
|
const program = safeParseSync(source, isTsx);
|
|
269
427
|
if (!program) {
|
|
270
|
-
return
|
|
428
|
+
return {
|
|
429
|
+
templates: [],
|
|
430
|
+
fieldTrees: []
|
|
431
|
+
};
|
|
271
432
|
}
|
|
272
433
|
const converter = createSwcSpanConverter(source);
|
|
273
434
|
const spanOffset = program.span.end - converter.byteLength + 1;
|
|
274
435
|
const filePath = uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
|
275
436
|
const gqlIdentifiers = collectGqlIdentifiers(program, filePath, helper);
|
|
276
437
|
if (gqlIdentifiers.size === 0) {
|
|
277
|
-
return
|
|
438
|
+
return {
|
|
439
|
+
templates: [],
|
|
440
|
+
fieldTrees: []
|
|
441
|
+
};
|
|
278
442
|
}
|
|
279
443
|
const positionCtx = {
|
|
280
444
|
spanOffset,
|
|
281
445
|
converter
|
|
282
446
|
};
|
|
283
|
-
|
|
447
|
+
const templates = walkAndExtract(program, gqlIdentifiers, positionCtx);
|
|
448
|
+
const fieldTrees = walkAndExtractFieldTrees(program, gqlIdentifiers, positionCtx);
|
|
449
|
+
return {
|
|
450
|
+
templates,
|
|
451
|
+
fieldTrees
|
|
452
|
+
};
|
|
284
453
|
};
|
|
285
454
|
return {
|
|
286
455
|
update: (uri, version, source) => {
|
|
287
|
-
const templates =
|
|
456
|
+
const { templates, fieldTrees } = extractAll(uri, source);
|
|
288
457
|
const state = {
|
|
289
458
|
uri,
|
|
290
459
|
version,
|
|
291
460
|
source,
|
|
292
461
|
templates,
|
|
462
|
+
fieldTrees,
|
|
293
463
|
...swcUnavailable ? { swcUnavailable: true } : {}
|
|
294
464
|
};
|
|
295
465
|
cache.set(uri, state);
|
|
@@ -312,6 +482,20 @@ const createDocumentManager = (helper, swcOptions) => {
|
|
|
312
482
|
}
|
|
313
483
|
return state.templates.find((t) => offset >= t.contentRange.start && offset <= t.contentRange.end);
|
|
314
484
|
},
|
|
485
|
+
findFieldTreeAtOffset: (uri, offset) => {
|
|
486
|
+
const state = cache.get(uri);
|
|
487
|
+
if (!state) {
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
return state.fieldTrees.find((t) => offset >= t.rootSpan.start && offset <= t.rootSpan.end);
|
|
491
|
+
},
|
|
492
|
+
findTemplateByTypeNameOffset: (uri, offset) => {
|
|
493
|
+
const state = cache.get(uri);
|
|
494
|
+
if (!state) {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
return state.templates.find((t) => t.typeNameSpan && offset >= t.typeNameSpan.start && offset <= t.typeNameSpan.end);
|
|
498
|
+
},
|
|
315
499
|
getExternalFragments: (uri, schemaName) => {
|
|
316
500
|
const result = [];
|
|
317
501
|
for (const [fragmentUri, fragments] of fragmentIndex) {
|
|
@@ -345,7 +529,7 @@ const createDocumentManager = (helper, swcOptions) => {
|
|
|
345
529
|
continue;
|
|
346
530
|
}
|
|
347
531
|
const reconstructed = reconstructGraphql(template);
|
|
348
|
-
const headerLen = reconstructed
|
|
532
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
349
533
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
350
534
|
try {
|
|
351
535
|
const ast = parse(preprocessed, { noLocation: false });
|
|
@@ -380,50 +564,6 @@ const createDocumentManager = (helper, swcOptions) => {
|
|
|
380
564
|
};
|
|
381
565
|
};
|
|
382
566
|
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region packages/lsp/src/errors.ts
|
|
385
|
-
/** Error constructor helpers for concise error creation. */
|
|
386
|
-
const lspErrors = {
|
|
387
|
-
configLoadFailed: (message, cause) => ({
|
|
388
|
-
code: "CONFIG_LOAD_FAILED",
|
|
389
|
-
message,
|
|
390
|
-
cause
|
|
391
|
-
}),
|
|
392
|
-
schemaLoadFailed: (schemaName, message, cause) => ({
|
|
393
|
-
code: "SCHEMA_LOAD_FAILED",
|
|
394
|
-
message: message ?? `Failed to load schema: ${schemaName}`,
|
|
395
|
-
schemaName,
|
|
396
|
-
cause
|
|
397
|
-
}),
|
|
398
|
-
schemaBuildFailed: (schemaName, message, cause) => ({
|
|
399
|
-
code: "SCHEMA_BUILD_FAILED",
|
|
400
|
-
message: message ?? `Failed to build schema: ${schemaName}`,
|
|
401
|
-
schemaName,
|
|
402
|
-
cause
|
|
403
|
-
}),
|
|
404
|
-
schemaNotConfigured: (schemaName) => ({
|
|
405
|
-
code: "SCHEMA_NOT_CONFIGURED",
|
|
406
|
-
message: `Schema "${schemaName}" is not configured in soda-gql.config`,
|
|
407
|
-
schemaName
|
|
408
|
-
}),
|
|
409
|
-
parseFailed: (uri, message, cause) => ({
|
|
410
|
-
code: "PARSE_FAILED",
|
|
411
|
-
message: message ?? `Failed to parse: ${uri}`,
|
|
412
|
-
uri,
|
|
413
|
-
cause
|
|
414
|
-
}),
|
|
415
|
-
internalInvariant: (message, context, cause) => ({
|
|
416
|
-
code: "INTERNAL_INVARIANT",
|
|
417
|
-
message,
|
|
418
|
-
context,
|
|
419
|
-
cause
|
|
420
|
-
}),
|
|
421
|
-
swcResolutionFailed: (message) => ({
|
|
422
|
-
code: "SWC_RESOLUTION_FAILED",
|
|
423
|
-
message: message ?? "@swc/core not found. Install @soda-gql/builder (which provides @swc/core) in your project and restart the LSP server to enable template extraction."
|
|
424
|
-
})
|
|
425
|
-
};
|
|
426
|
-
|
|
427
567
|
//#endregion
|
|
428
568
|
//#region packages/lsp/src/position-mapping.ts
|
|
429
569
|
/** Compute byte offsets for the start of each line in the source text. */
|
|
@@ -501,164 +641,67 @@ const createPositionMapper = (input) => {
|
|
|
501
641
|
};
|
|
502
642
|
|
|
503
643
|
//#endregion
|
|
504
|
-
//#region packages/lsp/src/
|
|
505
|
-
/**
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
};
|
|
517
|
-
const loadAndBuildSchema = (schemaName, schemaPaths) => {
|
|
518
|
-
const documents = [];
|
|
519
|
-
const files = [];
|
|
520
|
-
for (const schemaPath of schemaPaths) {
|
|
521
|
-
const resolvedPath = resolve(schemaPath);
|
|
522
|
-
try {
|
|
523
|
-
const content = readFileSync(resolvedPath, "utf8");
|
|
524
|
-
documents.push(parse(content));
|
|
525
|
-
files.push({
|
|
526
|
-
filePath: resolvedPath,
|
|
527
|
-
content
|
|
528
|
-
});
|
|
529
|
-
} catch (e) {
|
|
530
|
-
return err(lspErrors.schemaLoadFailed(schemaName, e instanceof Error ? e.message : String(e)));
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const documentNode = concatAST(documents);
|
|
534
|
-
const hash = hashSchema(documentNode);
|
|
535
|
-
const buildResult = safeBuildASTSchema(schemaName, documentNode);
|
|
536
|
-
if (buildResult.isErr()) {
|
|
537
|
-
return err(buildResult.error);
|
|
538
|
-
}
|
|
539
|
-
return ok({
|
|
540
|
-
name: schemaName,
|
|
541
|
-
schema: buildResult.value,
|
|
542
|
-
documentNode,
|
|
543
|
-
hash,
|
|
544
|
-
files
|
|
644
|
+
//#region packages/lsp/src/handlers/diagnostics.ts
|
|
645
|
+
/** Compute LSP diagnostics for a single GraphQL template. */
|
|
646
|
+
const computeTemplateDiagnostics = (input) => {
|
|
647
|
+
const { template, schema, tsSource } = input;
|
|
648
|
+
const reconstructed = reconstructGraphql(template);
|
|
649
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
650
|
+
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
651
|
+
const mapper = createPositionMapper({
|
|
652
|
+
tsSource,
|
|
653
|
+
contentStartOffset: template.contentRange.start,
|
|
654
|
+
graphqlContent: template.content
|
|
545
655
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
656
|
+
const gqlDiagnostics = getDiagnostics(preprocessed, schema, undefined, undefined, input.externalFragments);
|
|
657
|
+
const placeholderPattern = /__FRAG_SPREAD_\d+__/;
|
|
658
|
+
const isCallbackVariables = template.source === "callback-variables";
|
|
659
|
+
const reconstructedLineOffsets = computeLineOffsets(preprocessed);
|
|
660
|
+
const contentLineOffsets = computeLineOffsets(template.content);
|
|
661
|
+
const toContentPosition = (pos) => {
|
|
662
|
+
const offset = positionToOffset$1(reconstructedLineOffsets, pos);
|
|
663
|
+
const contentOffset = Math.max(0, offset - headerLen);
|
|
664
|
+
return offsetToPosition(contentLineOffsets, contentOffset);
|
|
665
|
+
};
|
|
666
|
+
return gqlDiagnostics.filter((diag) => {
|
|
667
|
+
if (placeholderPattern.test(diag.message)) {
|
|
668
|
+
return false;
|
|
554
669
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
if (result.isErr()) {
|
|
567
|
-
return err(result.error);
|
|
568
|
-
}
|
|
569
|
-
cache.set(schemaName, result.value);
|
|
570
|
-
return ok(result.value);
|
|
571
|
-
},
|
|
572
|
-
reloadAll: () => {
|
|
573
|
-
const errors = [];
|
|
574
|
-
for (const [name, schemaConfig] of Object.entries(config.schemas)) {
|
|
575
|
-
const result = loadAndBuildSchema(name, schemaConfig.schema);
|
|
576
|
-
if (result.isErr()) {
|
|
577
|
-
errors.push(result.error);
|
|
578
|
-
} else {
|
|
579
|
-
cache.set(name, result.value);
|
|
580
|
-
}
|
|
670
|
+
if (isCallbackVariables && diag.message.includes("is never used")) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const offset = positionToOffset$1(reconstructedLineOffsets, diag.range.start);
|
|
674
|
+
if (offset < headerLen) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
if (isCallbackVariables) {
|
|
678
|
+
const contentEnd = headerLen + template.content.length;
|
|
679
|
+
if (offset >= contentEnd) {
|
|
680
|
+
return false;
|
|
581
681
|
}
|
|
582
|
-
return errors.length > 0 ? err(errors) : ok(undefined);
|
|
583
682
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (resolverResult.isErr()) {
|
|
607
|
-
return err(resolverResult.error);
|
|
608
|
-
}
|
|
609
|
-
contexts.set(configPath, {
|
|
610
|
-
configPath,
|
|
611
|
-
config,
|
|
612
|
-
helper,
|
|
613
|
-
schemaResolver: resolverResult.value,
|
|
614
|
-
documentManager: createDocumentManager(helper, { resolveFrom: configPath })
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
const uriCache = new Map();
|
|
618
|
-
const resolveConfigPath = (dirPath) => {
|
|
619
|
-
const cached = uriCache.get(dirPath);
|
|
620
|
-
if (cached !== undefined) {
|
|
621
|
-
return cached;
|
|
622
|
-
}
|
|
623
|
-
for (const configPath of sortedPaths) {
|
|
624
|
-
const configDir = dirname(configPath);
|
|
625
|
-
if (dirPath === configDir || dirPath.startsWith(`${configDir}${sep}`)) {
|
|
626
|
-
uriCache.set(dirPath, configPath);
|
|
627
|
-
return configPath;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
uriCache.set(dirPath, null);
|
|
631
|
-
return null;
|
|
632
|
-
};
|
|
633
|
-
return ok({
|
|
634
|
-
resolveForUri: (uri) => {
|
|
635
|
-
if (!uri.startsWith("file://") && !uri.startsWith("/")) {
|
|
636
|
-
return undefined;
|
|
637
|
-
}
|
|
638
|
-
const filePath = uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
|
639
|
-
const dirPath = dirname(filePath);
|
|
640
|
-
const configPath = resolveConfigPath(dirPath);
|
|
641
|
-
return configPath ? contexts.get(configPath) : undefined;
|
|
642
|
-
},
|
|
643
|
-
getAllContexts: () => [...contexts.values()],
|
|
644
|
-
reloadSchemas: (configPath) => {
|
|
645
|
-
const ctx = contexts.get(configPath);
|
|
646
|
-
if (!ctx) {
|
|
647
|
-
return err([lspErrors.configLoadFailed(`Config not found: ${configPath}`)]);
|
|
648
|
-
}
|
|
649
|
-
return ctx.schemaResolver.reloadAll();
|
|
650
|
-
},
|
|
651
|
-
reloadAllSchemas: () => {
|
|
652
|
-
const errors = [];
|
|
653
|
-
for (const ctx of contexts.values()) {
|
|
654
|
-
const result = ctx.schemaResolver.reloadAll();
|
|
655
|
-
if (result.isErr()) {
|
|
656
|
-
errors.push(...result.error);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
return errors.length > 0 ? err(errors) : ok(undefined);
|
|
660
|
-
}
|
|
661
|
-
});
|
|
683
|
+
return true;
|
|
684
|
+
}).map((diag) => {
|
|
685
|
+
const startContent = toContentPosition(diag.range.start);
|
|
686
|
+
const endContent = toContentPosition(diag.range.end);
|
|
687
|
+
const startTs = mapper.graphqlToTs(startContent);
|
|
688
|
+
const endTs = mapper.graphqlToTs(endContent);
|
|
689
|
+
return {
|
|
690
|
+
range: {
|
|
691
|
+
start: {
|
|
692
|
+
line: startTs.line,
|
|
693
|
+
character: startTs.character
|
|
694
|
+
},
|
|
695
|
+
end: {
|
|
696
|
+
line: endTs.line,
|
|
697
|
+
character: endTs.character
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
message: diag.message,
|
|
701
|
+
severity: diag.severity,
|
|
702
|
+
source: "soda-gql"
|
|
703
|
+
};
|
|
704
|
+
});
|
|
662
705
|
};
|
|
663
706
|
|
|
664
707
|
//#endregion
|
|
@@ -2921,6 +2964,303 @@ var Is;
|
|
|
2921
2964
|
Is$1.typedArray = typedArray;
|
|
2922
2965
|
})(Is || (Is = {}));
|
|
2923
2966
|
|
|
2967
|
+
//#endregion
|
|
2968
|
+
//#region packages/lsp/src/handlers/field-tree-diagnostics.ts
|
|
2969
|
+
/**
|
|
2970
|
+
* Diagnostics handler for callback builder field tree nodes.
|
|
2971
|
+
* Reports unknown fields and invalid union member types.
|
|
2972
|
+
* @module
|
|
2973
|
+
*/
|
|
2974
|
+
/** Compute LSP diagnostics for a callback builder field tree. */
|
|
2975
|
+
const computeFieldTreeDiagnostics = (input) => {
|
|
2976
|
+
const { fieldTree, tsSource } = input;
|
|
2977
|
+
const lineOffsets = computeLineOffsets(tsSource);
|
|
2978
|
+
const diagnostics = [];
|
|
2979
|
+
const walkNodes = (nodes) => {
|
|
2980
|
+
for (const node of nodes) {
|
|
2981
|
+
if (node.fieldTypeName === null) {
|
|
2982
|
+
const start = offsetToPosition(lineOffsets, node.fieldNameSpan.start);
|
|
2983
|
+
const end = offsetToPosition(lineOffsets, node.fieldNameSpan.end);
|
|
2984
|
+
diagnostics.push({
|
|
2985
|
+
range: {
|
|
2986
|
+
start,
|
|
2987
|
+
end
|
|
2988
|
+
},
|
|
2989
|
+
message: `Unknown field "${node.fieldName}" on type "${node.parentTypeName}"`,
|
|
2990
|
+
severity: DiagnosticSeverity.Error,
|
|
2991
|
+
source: "soda-gql"
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
if (node.nested) {
|
|
2995
|
+
walkNested(node.nested);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
};
|
|
2999
|
+
const walkNested = (nested) => {
|
|
3000
|
+
if (nested.kind === "object") {
|
|
3001
|
+
walkNodes(nested.children);
|
|
3002
|
+
} else {
|
|
3003
|
+
for (const branch of nested.branches) {
|
|
3004
|
+
if (!branch.valid) {
|
|
3005
|
+
const start = offsetToPosition(lineOffsets, branch.typeNameSpan.start);
|
|
3006
|
+
const end = offsetToPosition(lineOffsets, branch.typeNameSpan.end);
|
|
3007
|
+
diagnostics.push({
|
|
3008
|
+
range: {
|
|
3009
|
+
start,
|
|
3010
|
+
end
|
|
3011
|
+
},
|
|
3012
|
+
message: `Type "${branch.typeName}" is not a member of union type`,
|
|
3013
|
+
severity: DiagnosticSeverity.Error,
|
|
3014
|
+
source: "soda-gql"
|
|
3015
|
+
});
|
|
3016
|
+
}
|
|
3017
|
+
walkNodes(branch.children);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
walkNodes(fieldTree.children);
|
|
3022
|
+
return diagnostics;
|
|
3023
|
+
};
|
|
3024
|
+
|
|
3025
|
+
//#endregion
|
|
3026
|
+
//#region packages/lsp/src/diagnostics-collector.ts
|
|
3027
|
+
/** Collect all diagnostics (template + field tree) for a document state. */
|
|
3028
|
+
const collectRawDiagnostics = (state, ctx) => {
|
|
3029
|
+
const templateDiagnostics = state.templates.flatMap((template) => {
|
|
3030
|
+
const entry = ctx.schemaResolver.getSchema(template.schemaName);
|
|
3031
|
+
if (!entry) {
|
|
3032
|
+
return [];
|
|
3033
|
+
}
|
|
3034
|
+
const externalFragments = ctx.documentManager.getExternalFragments(state.uri, template.schemaName).map((f) => f.definition);
|
|
3035
|
+
return [...computeTemplateDiagnostics({
|
|
3036
|
+
template,
|
|
3037
|
+
schema: entry.schema,
|
|
3038
|
+
tsSource: state.source,
|
|
3039
|
+
externalFragments
|
|
3040
|
+
})];
|
|
3041
|
+
});
|
|
3042
|
+
const fieldTreeDiagnostics = state.fieldTrees.flatMap((tree) => {
|
|
3043
|
+
const entry = ctx.schemaResolver.getSchema(tree.schemaName);
|
|
3044
|
+
if (!entry) {
|
|
3045
|
+
return [];
|
|
3046
|
+
}
|
|
3047
|
+
const typedTree = resolveFieldTree(tree, entry.schema);
|
|
3048
|
+
if (!typedTree) {
|
|
3049
|
+
return [];
|
|
3050
|
+
}
|
|
3051
|
+
return [...computeFieldTreeDiagnostics({
|
|
3052
|
+
fieldTree: typedTree,
|
|
3053
|
+
tsSource: state.source
|
|
3054
|
+
})];
|
|
3055
|
+
});
|
|
3056
|
+
return [...templateDiagnostics, ...fieldTreeDiagnostics];
|
|
3057
|
+
};
|
|
3058
|
+
|
|
3059
|
+
//#endregion
|
|
3060
|
+
//#region packages/lsp/src/errors.ts
|
|
3061
|
+
/** Error constructor helpers for concise error creation. */
|
|
3062
|
+
const lspErrors = {
|
|
3063
|
+
configLoadFailed: (message, cause) => ({
|
|
3064
|
+
code: "CONFIG_LOAD_FAILED",
|
|
3065
|
+
message,
|
|
3066
|
+
cause
|
|
3067
|
+
}),
|
|
3068
|
+
schemaLoadFailed: (schemaName, message, cause) => ({
|
|
3069
|
+
code: "SCHEMA_LOAD_FAILED",
|
|
3070
|
+
message: message ?? `Failed to load schema: ${schemaName}`,
|
|
3071
|
+
schemaName,
|
|
3072
|
+
cause
|
|
3073
|
+
}),
|
|
3074
|
+
schemaBuildFailed: (schemaName, message, cause) => ({
|
|
3075
|
+
code: "SCHEMA_BUILD_FAILED",
|
|
3076
|
+
message: message ?? `Failed to build schema: ${schemaName}`,
|
|
3077
|
+
schemaName,
|
|
3078
|
+
cause
|
|
3079
|
+
}),
|
|
3080
|
+
schemaNotConfigured: (schemaName) => ({
|
|
3081
|
+
code: "SCHEMA_NOT_CONFIGURED",
|
|
3082
|
+
message: `Schema "${schemaName}" is not configured in soda-gql.config`,
|
|
3083
|
+
schemaName
|
|
3084
|
+
}),
|
|
3085
|
+
parseFailed: (uri, message, cause) => ({
|
|
3086
|
+
code: "PARSE_FAILED",
|
|
3087
|
+
message: message ?? `Failed to parse: ${uri}`,
|
|
3088
|
+
uri,
|
|
3089
|
+
cause
|
|
3090
|
+
}),
|
|
3091
|
+
internalInvariant: (message, context, cause) => ({
|
|
3092
|
+
code: "INTERNAL_INVARIANT",
|
|
3093
|
+
message,
|
|
3094
|
+
context,
|
|
3095
|
+
cause
|
|
3096
|
+
}),
|
|
3097
|
+
swcResolutionFailed: (message) => ({
|
|
3098
|
+
code: "SWC_RESOLUTION_FAILED",
|
|
3099
|
+
message: message ?? "@swc/core not found. Install @soda-gql/builder (which provides @swc/core) in your project and restart the LSP server to enable template extraction."
|
|
3100
|
+
})
|
|
3101
|
+
};
|
|
3102
|
+
|
|
3103
|
+
//#endregion
|
|
3104
|
+
//#region packages/lsp/src/schema-resolver.ts
|
|
3105
|
+
/**
|
|
3106
|
+
* Schema resolver: maps schema names to GraphQLSchema objects.
|
|
3107
|
+
* @module
|
|
3108
|
+
*/
|
|
3109
|
+
/** Wrap buildASTSchema (which throws) in a Result. */
|
|
3110
|
+
const safeBuildASTSchema = (schemaName, documentNode) => {
|
|
3111
|
+
try {
|
|
3112
|
+
return ok(buildASTSchema(documentNode));
|
|
3113
|
+
} catch (e) {
|
|
3114
|
+
return err(lspErrors.schemaBuildFailed(schemaName, e instanceof Error ? e.message : String(e), e));
|
|
3115
|
+
}
|
|
3116
|
+
};
|
|
3117
|
+
const loadAndBuildSchema = (schemaName, schemaPaths) => {
|
|
3118
|
+
const documents = [];
|
|
3119
|
+
const files = [];
|
|
3120
|
+
for (const schemaPath of schemaPaths) {
|
|
3121
|
+
const resolvedPath = resolve(schemaPath);
|
|
3122
|
+
try {
|
|
3123
|
+
const content = readFileSync(resolvedPath, "utf8");
|
|
3124
|
+
documents.push(parse(content));
|
|
3125
|
+
files.push({
|
|
3126
|
+
filePath: resolvedPath,
|
|
3127
|
+
content
|
|
3128
|
+
});
|
|
3129
|
+
} catch (e) {
|
|
3130
|
+
return err(lspErrors.schemaLoadFailed(schemaName, e instanceof Error ? e.message : String(e)));
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
const documentNode = concatAST(documents);
|
|
3134
|
+
const hash = hashSchema(documentNode);
|
|
3135
|
+
const buildResult = safeBuildASTSchema(schemaName, documentNode);
|
|
3136
|
+
if (buildResult.isErr()) {
|
|
3137
|
+
return err(buildResult.error);
|
|
3138
|
+
}
|
|
3139
|
+
return ok({
|
|
3140
|
+
name: schemaName,
|
|
3141
|
+
schema: buildResult.value,
|
|
3142
|
+
documentNode,
|
|
3143
|
+
hash,
|
|
3144
|
+
files
|
|
3145
|
+
});
|
|
3146
|
+
};
|
|
3147
|
+
/** Create a schema resolver from config. Loads all schemas eagerly. */
|
|
3148
|
+
const createSchemaResolver = (config) => {
|
|
3149
|
+
const cache = new Map();
|
|
3150
|
+
for (const [name, schemaConfig] of Object.entries(config.schemas)) {
|
|
3151
|
+
const result = loadAndBuildSchema(name, schemaConfig.schema);
|
|
3152
|
+
if (result.isErr()) {
|
|
3153
|
+
return err(result.error);
|
|
3154
|
+
}
|
|
3155
|
+
cache.set(name, result.value);
|
|
3156
|
+
}
|
|
3157
|
+
const resolver = {
|
|
3158
|
+
getSchema: (schemaName) => cache.get(schemaName),
|
|
3159
|
+
getSchemaNames: () => [...cache.keys()],
|
|
3160
|
+
reloadSchema: (schemaName) => {
|
|
3161
|
+
const schemaConfig = config.schemas[schemaName];
|
|
3162
|
+
if (!schemaConfig) {
|
|
3163
|
+
return err(lspErrors.schemaNotConfigured(schemaName));
|
|
3164
|
+
}
|
|
3165
|
+
const result = loadAndBuildSchema(schemaName, schemaConfig.schema);
|
|
3166
|
+
if (result.isErr()) {
|
|
3167
|
+
return err(result.error);
|
|
3168
|
+
}
|
|
3169
|
+
cache.set(schemaName, result.value);
|
|
3170
|
+
return ok(result.value);
|
|
3171
|
+
},
|
|
3172
|
+
reloadAll: () => {
|
|
3173
|
+
const errors = [];
|
|
3174
|
+
for (const [name, schemaConfig] of Object.entries(config.schemas)) {
|
|
3175
|
+
const result = loadAndBuildSchema(name, schemaConfig.schema);
|
|
3176
|
+
if (result.isErr()) {
|
|
3177
|
+
errors.push(result.error);
|
|
3178
|
+
} else {
|
|
3179
|
+
cache.set(name, result.value);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
return errors.length > 0 ? err(errors) : ok(undefined);
|
|
3183
|
+
}
|
|
3184
|
+
};
|
|
3185
|
+
return ok(resolver);
|
|
3186
|
+
};
|
|
3187
|
+
|
|
3188
|
+
//#endregion
|
|
3189
|
+
//#region packages/lsp/src/config-registry.ts
|
|
3190
|
+
/**
|
|
3191
|
+
* Config registry: maps document URIs to their nearest config context.
|
|
3192
|
+
* Supports multiple soda-gql configs in a monorepo workspace.
|
|
3193
|
+
* @module
|
|
3194
|
+
*/
|
|
3195
|
+
const createConfigRegistry = (configPaths) => {
|
|
3196
|
+
const sortedPaths = [...configPaths].sort((a, b) => b.length - a.length);
|
|
3197
|
+
const contexts = new Map();
|
|
3198
|
+
for (const configPath of sortedPaths) {
|
|
3199
|
+
const configResult = loadConfig(configPath);
|
|
3200
|
+
if (configResult.isErr()) {
|
|
3201
|
+
return err(lspErrors.configLoadFailed(`Failed to load config ${configPath}: ${configResult.error.message}`, configResult.error));
|
|
3202
|
+
}
|
|
3203
|
+
const config = configResult.value;
|
|
3204
|
+
const helper = createGraphqlSystemIdentifyHelper(config);
|
|
3205
|
+
const resolverResult = createSchemaResolver(config);
|
|
3206
|
+
if (resolverResult.isErr()) {
|
|
3207
|
+
return err(resolverResult.error);
|
|
3208
|
+
}
|
|
3209
|
+
contexts.set(configPath, {
|
|
3210
|
+
configPath,
|
|
3211
|
+
config,
|
|
3212
|
+
helper,
|
|
3213
|
+
schemaResolver: resolverResult.value,
|
|
3214
|
+
documentManager: createDocumentManager(helper, { resolveFrom: configPath })
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
const uriCache = new Map();
|
|
3218
|
+
const resolveConfigPath = (dirPath) => {
|
|
3219
|
+
const cached = uriCache.get(dirPath);
|
|
3220
|
+
if (cached !== undefined) {
|
|
3221
|
+
return cached;
|
|
3222
|
+
}
|
|
3223
|
+
for (const configPath of sortedPaths) {
|
|
3224
|
+
const configDir = dirname(configPath);
|
|
3225
|
+
if (dirPath === configDir || dirPath.startsWith(`${configDir}${sep}`)) {
|
|
3226
|
+
uriCache.set(dirPath, configPath);
|
|
3227
|
+
return configPath;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
uriCache.set(dirPath, null);
|
|
3231
|
+
return null;
|
|
3232
|
+
};
|
|
3233
|
+
return ok({
|
|
3234
|
+
resolveForUri: (uri) => {
|
|
3235
|
+
if (!uri.startsWith("file://") && !uri.startsWith("/")) {
|
|
3236
|
+
return undefined;
|
|
3237
|
+
}
|
|
3238
|
+
const filePath = uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
|
3239
|
+
const dirPath = dirname(filePath);
|
|
3240
|
+
const configPath = resolveConfigPath(dirPath);
|
|
3241
|
+
return configPath ? contexts.get(configPath) : undefined;
|
|
3242
|
+
},
|
|
3243
|
+
getAllContexts: () => [...contexts.values()],
|
|
3244
|
+
reloadSchemas: (configPath) => {
|
|
3245
|
+
const ctx = contexts.get(configPath);
|
|
3246
|
+
if (!ctx) {
|
|
3247
|
+
return err([lspErrors.configLoadFailed(`Config not found: ${configPath}`)]);
|
|
3248
|
+
}
|
|
3249
|
+
return ctx.schemaResolver.reloadAll();
|
|
3250
|
+
},
|
|
3251
|
+
reloadAllSchemas: () => {
|
|
3252
|
+
const errors = [];
|
|
3253
|
+
for (const ctx of contexts.values()) {
|
|
3254
|
+
const result = ctx.schemaResolver.reloadAll();
|
|
3255
|
+
if (result.isErr()) {
|
|
3256
|
+
errors.push(...result.error);
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
return errors.length > 0 ? err(errors) : ok(undefined);
|
|
3260
|
+
}
|
|
3261
|
+
});
|
|
3262
|
+
};
|
|
3263
|
+
|
|
2924
3264
|
//#endregion
|
|
2925
3265
|
//#region packages/lsp/src/handlers/code-action.ts
|
|
2926
3266
|
/** Handle a code action request for a GraphQL template. */
|
|
@@ -3078,7 +3418,7 @@ const findStatementStart = (source, offset) => {
|
|
|
3078
3418
|
const handleCompletion = (input) => {
|
|
3079
3419
|
const { template, schema, tsSource, tsPosition } = input;
|
|
3080
3420
|
const reconstructed = reconstructGraphql(template);
|
|
3081
|
-
const headerLen = reconstructed
|
|
3421
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
3082
3422
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
3083
3423
|
const mapper = createPositionMapper({
|
|
3084
3424
|
tsSource,
|
|
@@ -3101,6 +3441,27 @@ const handleCompletion = (input) => {
|
|
|
3101
3441
|
//#endregion
|
|
3102
3442
|
//#region packages/lsp/src/handlers/_utils.ts
|
|
3103
3443
|
/**
|
|
3444
|
+
* Shared handler utilities for fragment spread and definition lookup.
|
|
3445
|
+
* @module
|
|
3446
|
+
*/
|
|
3447
|
+
/** Build ObjectTypeInfo[] from schema file info for graphql-language-service definition APIs. */
|
|
3448
|
+
const buildObjectTypeInfos = (files) => {
|
|
3449
|
+
const result = [];
|
|
3450
|
+
for (const file of files) {
|
|
3451
|
+
const doc = parse(file.content);
|
|
3452
|
+
for (const def of doc.definitions) {
|
|
3453
|
+
if (isTypeDefinitionNode(def)) {
|
|
3454
|
+
result.push({
|
|
3455
|
+
filePath: pathToFileURL(file.filePath).href,
|
|
3456
|
+
content: file.content,
|
|
3457
|
+
definition: def
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return result;
|
|
3463
|
+
};
|
|
3464
|
+
/**
|
|
3104
3465
|
* Find the fragment spread node at the given GraphQL offset using AST.
|
|
3105
3466
|
* Returns the FragmentSpreadNode if cursor is on a `...FragmentName` pattern.
|
|
3106
3467
|
*/
|
|
@@ -3230,35 +3591,41 @@ const computeSpreadLocationRanges = (spreadLocations) => {
|
|
|
3230
3591
|
}
|
|
3231
3592
|
return ranges;
|
|
3232
3593
|
};
|
|
3233
|
-
|
|
3234
|
-
//#endregion
|
|
3235
|
-
//#region packages/lsp/src/handlers/definition.ts
|
|
3236
3594
|
/**
|
|
3237
|
-
*
|
|
3238
|
-
*
|
|
3595
|
+
* Resolve a directive name to its definition in schema files.
|
|
3596
|
+
* Parses each schema file and finds DirectiveDefinitionNode matching the name.
|
|
3239
3597
|
*/
|
|
3240
|
-
|
|
3241
|
-
const
|
|
3242
|
-
const
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3598
|
+
const resolveDirectiveDefinition = (directiveName, schemaFiles) => {
|
|
3599
|
+
const locations = [];
|
|
3600
|
+
for (const file of schemaFiles) {
|
|
3601
|
+
try {
|
|
3602
|
+
const doc = parse(file.content, { noLocation: false });
|
|
3603
|
+
const lineOffsets = computeLineOffsets(file.content);
|
|
3604
|
+
for (const def of doc.definitions) {
|
|
3605
|
+
if (def.kind === "DirectiveDefinition" && def.name.value === directiveName && def.name.loc) {
|
|
3606
|
+
const start = offsetToPosition(lineOffsets, def.name.loc.start);
|
|
3607
|
+
const end = offsetToPosition(lineOffsets, def.name.loc.end);
|
|
3608
|
+
locations.push({
|
|
3609
|
+
uri: pathToFileURL(file.filePath).href,
|
|
3610
|
+
range: {
|
|
3611
|
+
start,
|
|
3612
|
+
end
|
|
3613
|
+
}
|
|
3614
|
+
});
|
|
3615
|
+
}
|
|
3252
3616
|
}
|
|
3253
|
-
}
|
|
3617
|
+
} catch {}
|
|
3254
3618
|
}
|
|
3255
|
-
return
|
|
3619
|
+
return locations;
|
|
3256
3620
|
};
|
|
3621
|
+
|
|
3622
|
+
//#endregion
|
|
3623
|
+
//#region packages/lsp/src/handlers/definition.ts
|
|
3257
3624
|
/** Handle a definition request for a GraphQL template. */
|
|
3258
3625
|
const handleDefinition = async (input) => {
|
|
3259
3626
|
const { template, tsSource, tsPosition, externalFragments } = input;
|
|
3260
3627
|
const reconstructed = reconstructGraphql(template);
|
|
3261
|
-
const headerLen = reconstructed
|
|
3628
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
3262
3629
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
3263
3630
|
const mapper = createPositionMapper({
|
|
3264
3631
|
tsSource,
|
|
@@ -3276,10 +3643,16 @@ const handleDefinition = async (input) => {
|
|
|
3276
3643
|
if (fragmentSpread) {
|
|
3277
3644
|
return resolveFragmentSpreadDefinition(preprocessed, fragmentSpread, externalFragments);
|
|
3278
3645
|
}
|
|
3646
|
+
if (input.schemaFiles && input.schemaFiles.length > 0) {
|
|
3647
|
+
const varTypeResult = await resolveVariableTypeDefinition(preprocessed, reconstructedOffset, input.schemaFiles);
|
|
3648
|
+
if (varTypeResult.length > 0) {
|
|
3649
|
+
return varTypeResult;
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3279
3652
|
if (input.schema && input.schemaFiles && input.schemaFiles.length > 0) {
|
|
3280
3653
|
const reconstructedLineOffsets = computeLineOffsets(preprocessed);
|
|
3281
3654
|
const reconstructedPosition = offsetToPosition(reconstructedLineOffsets, reconstructedOffset);
|
|
3282
|
-
return resolveSchemaDefinition(preprocessed, reconstructedPosition, input.schema, input.schemaFiles);
|
|
3655
|
+
return resolveSchemaDefinition(preprocessed, reconstructedPosition, reconstructedOffset, input.schema, input.schemaFiles);
|
|
3283
3656
|
}
|
|
3284
3657
|
return [];
|
|
3285
3658
|
};
|
|
@@ -3338,28 +3711,84 @@ const resolveFragmentSpreadDefinition = async (preprocessed, fragmentSpread, ext
|
|
|
3338
3711
|
character: endChar
|
|
3339
3712
|
}
|
|
3340
3713
|
}
|
|
3341
|
-
};
|
|
3342
|
-
});
|
|
3714
|
+
};
|
|
3715
|
+
});
|
|
3716
|
+
} catch {
|
|
3717
|
+
return [];
|
|
3718
|
+
}
|
|
3719
|
+
};
|
|
3720
|
+
/** Unwrap NonNullType/ListType wrappers to get the inner NamedType node. */
|
|
3721
|
+
const unwrapToNamedType = (typeNode) => {
|
|
3722
|
+
if (typeNode.kind === "NamedType") return typeNode;
|
|
3723
|
+
if (typeNode.kind === "NonNullType" || typeNode.kind === "ListType") {
|
|
3724
|
+
return unwrapToNamedType(typeNode.type);
|
|
3725
|
+
}
|
|
3726
|
+
return null;
|
|
3727
|
+
};
|
|
3728
|
+
/**
|
|
3729
|
+
* Resolve variable type reference to its definition in a schema file.
|
|
3730
|
+
* Parses the GraphQL source to find VariableDefinition nodes, checks if the
|
|
3731
|
+
* cursor offset falls within a type name, and resolves via getDefinitionQueryResultForNamedType.
|
|
3732
|
+
*/
|
|
3733
|
+
const resolveVariableTypeDefinition = async (preprocessed, reconstructedOffset, schemaFiles) => {
|
|
3734
|
+
let ast;
|
|
3735
|
+
try {
|
|
3736
|
+
ast = parse(preprocessed, { noLocation: false });
|
|
3737
|
+
} catch {
|
|
3738
|
+
return [];
|
|
3739
|
+
}
|
|
3740
|
+
let matchedNode = null;
|
|
3741
|
+
visit(ast, { VariableDefinition(node) {
|
|
3742
|
+
if (matchedNode) return;
|
|
3743
|
+
const namedType = unwrapToNamedType(node.type);
|
|
3744
|
+
if (!namedType?.name.loc) return;
|
|
3745
|
+
const { start, end } = namedType.name.loc;
|
|
3746
|
+
if (reconstructedOffset >= start && reconstructedOffset < end) {
|
|
3747
|
+
matchedNode = namedType;
|
|
3748
|
+
}
|
|
3749
|
+
} });
|
|
3750
|
+
if (!matchedNode) return [];
|
|
3751
|
+
const objectTypeInfos = buildObjectTypeInfos(schemaFiles);
|
|
3752
|
+
try {
|
|
3753
|
+
const result = await getDefinitionQueryResultForNamedType(preprocessed, matchedNode, objectTypeInfos);
|
|
3754
|
+
return result.definitions.map((def) => ({
|
|
3755
|
+
uri: def.path ?? "",
|
|
3756
|
+
range: {
|
|
3757
|
+
start: {
|
|
3758
|
+
line: def.position.line,
|
|
3759
|
+
character: def.position.character
|
|
3760
|
+
},
|
|
3761
|
+
end: {
|
|
3762
|
+
line: def.range?.end?.line ?? def.position.line,
|
|
3763
|
+
character: def.range?.end?.character ?? def.position.character
|
|
3764
|
+
}
|
|
3765
|
+
}
|
|
3766
|
+
}));
|
|
3343
3767
|
} catch {
|
|
3344
3768
|
return [];
|
|
3345
3769
|
}
|
|
3346
3770
|
};
|
|
3347
3771
|
/** Resolve field or type name to its definition in a schema .graphql file. */
|
|
3348
|
-
const resolveSchemaDefinition = (preprocessed, position, schema, schemaFiles) => {
|
|
3772
|
+
const resolveSchemaDefinition = async (preprocessed, position, reconstructedOffset, schema, schemaFiles) => {
|
|
3773
|
+
const astResult = await tryResolveTypeConditionOrDirective(preprocessed, reconstructedOffset, schemaFiles);
|
|
3774
|
+
if (astResult.matched) {
|
|
3775
|
+
return astResult.locations;
|
|
3776
|
+
}
|
|
3349
3777
|
const context = getContextAtPosition(preprocessed, toIPosition(position), schema);
|
|
3350
3778
|
if (!context) {
|
|
3351
|
-
return
|
|
3779
|
+
return [];
|
|
3352
3780
|
}
|
|
3353
3781
|
const { typeInfo } = context;
|
|
3354
3782
|
if (typeInfo.fieldDef && typeInfo.parentType) {
|
|
3355
3783
|
const fieldName = typeInfo.fieldDef.name;
|
|
3356
3784
|
const namedParentType = getNamedType(typeInfo.parentType);
|
|
3357
3785
|
if (!namedParentType) {
|
|
3358
|
-
return
|
|
3786
|
+
return [];
|
|
3359
3787
|
}
|
|
3360
3788
|
const parentTypeName = namedParentType.name;
|
|
3361
3789
|
const objectTypeInfos = buildObjectTypeInfos(schemaFiles);
|
|
3362
|
-
|
|
3790
|
+
const result = await getDefinitionQueryResultForField(fieldName, parentTypeName, objectTypeInfos);
|
|
3791
|
+
return result.definitions.map((def) => ({
|
|
3363
3792
|
uri: def.path ?? "",
|
|
3364
3793
|
range: {
|
|
3365
3794
|
start: {
|
|
@@ -3371,63 +3800,99 @@ const resolveSchemaDefinition = (preprocessed, position, schema, schemaFiles) =>
|
|
|
3371
3800
|
character: def.range?.end?.character ?? def.position.character
|
|
3372
3801
|
}
|
|
3373
3802
|
}
|
|
3374
|
-
}))
|
|
3803
|
+
}));
|
|
3375
3804
|
}
|
|
3376
|
-
return
|
|
3805
|
+
return [];
|
|
3377
3806
|
};
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3807
|
+
/**
|
|
3808
|
+
* Try to resolve inline fragment type conditions and directive names via AST walking.
|
|
3809
|
+
* Returns { matched: true, locations } when cursor is on a type condition or directive,
|
|
3810
|
+
* or { matched: false } when cursor is on neither (caller should try field resolution).
|
|
3811
|
+
*/
|
|
3812
|
+
const tryResolveTypeConditionOrDirective = async (preprocessed, reconstructedOffset, schemaFiles) => {
|
|
3813
|
+
let ast;
|
|
3814
|
+
try {
|
|
3815
|
+
ast = parse(preprocessed, { noLocation: false });
|
|
3816
|
+
} catch {
|
|
3817
|
+
return { matched: false };
|
|
3818
|
+
}
|
|
3819
|
+
let matchedTypeName = null;
|
|
3820
|
+
let matchedDirectiveName = null;
|
|
3821
|
+
visit(ast, {
|
|
3822
|
+
InlineFragment(node) {
|
|
3823
|
+
const loc = node.typeCondition?.name.loc;
|
|
3824
|
+
if (loc && reconstructedOffset >= loc.start && reconstructedOffset < loc.end) {
|
|
3825
|
+
matchedTypeName = node.typeCondition.name.value;
|
|
3826
|
+
}
|
|
3827
|
+
},
|
|
3828
|
+
FragmentDefinition(node) {
|
|
3829
|
+
const loc = node.typeCondition.name.loc;
|
|
3830
|
+
if (loc && reconstructedOffset >= loc.start && reconstructedOffset < loc.end) {
|
|
3831
|
+
matchedTypeName = node.typeCondition.name.value;
|
|
3832
|
+
}
|
|
3833
|
+
},
|
|
3834
|
+
Directive(node) {
|
|
3835
|
+
const loc = node.name.loc;
|
|
3836
|
+
if (loc && reconstructedOffset >= loc.start && reconstructedOffset < loc.end) {
|
|
3837
|
+
matchedDirectiveName = node.name.value;
|
|
3838
|
+
}
|
|
3408
3839
|
}
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3840
|
+
});
|
|
3841
|
+
if (matchedTypeName) {
|
|
3842
|
+
return {
|
|
3843
|
+
matched: true,
|
|
3844
|
+
locations: await resolveTypeNameToSchemaDefinition(matchedTypeName, schemaFiles)
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
if (matchedDirectiveName) {
|
|
3415
3848
|
return {
|
|
3849
|
+
matched: true,
|
|
3850
|
+
locations: resolveDirectiveDefinition(matchedDirectiveName, schemaFiles)
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
return { matched: false };
|
|
3854
|
+
};
|
|
3855
|
+
/**
|
|
3856
|
+
* Resolve a type name to its definition in a schema file.
|
|
3857
|
+
* Used for fragment type names and inline fragment type conditions.
|
|
3858
|
+
*/
|
|
3859
|
+
const resolveTypeNameToSchemaDefinition = async (typeName, schemaFiles) => {
|
|
3860
|
+
const typeNameLen = typeName.length;
|
|
3861
|
+
const dummyLoc = {
|
|
3862
|
+
start: 0,
|
|
3863
|
+
end: typeNameLen,
|
|
3864
|
+
startToken: null,
|
|
3865
|
+
endToken: null,
|
|
3866
|
+
source: null
|
|
3867
|
+
};
|
|
3868
|
+
const namedTypeNode = {
|
|
3869
|
+
kind: Kind.NAMED_TYPE,
|
|
3870
|
+
name: {
|
|
3871
|
+
kind: Kind.NAME,
|
|
3872
|
+
value: typeName,
|
|
3873
|
+
loc: dummyLoc
|
|
3874
|
+
},
|
|
3875
|
+
loc: dummyLoc
|
|
3876
|
+
};
|
|
3877
|
+
const objectTypeInfos = buildObjectTypeInfos(schemaFiles);
|
|
3878
|
+
try {
|
|
3879
|
+
const result = await getDefinitionQueryResultForNamedType("", namedTypeNode, objectTypeInfos);
|
|
3880
|
+
return result.definitions.map((def) => ({
|
|
3881
|
+
uri: def.path ?? "",
|
|
3416
3882
|
range: {
|
|
3417
3883
|
start: {
|
|
3418
|
-
line:
|
|
3419
|
-
character:
|
|
3884
|
+
line: def.position.line,
|
|
3885
|
+
character: def.position.character
|
|
3420
3886
|
},
|
|
3421
3887
|
end: {
|
|
3422
|
-
line:
|
|
3423
|
-
character:
|
|
3888
|
+
line: def.range?.end?.line ?? def.position.line,
|
|
3889
|
+
character: def.range?.end?.character ?? def.position.character
|
|
3424
3890
|
}
|
|
3425
|
-
}
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
});
|
|
3891
|
+
}
|
|
3892
|
+
}));
|
|
3893
|
+
} catch {
|
|
3894
|
+
return [];
|
|
3895
|
+
}
|
|
3431
3896
|
};
|
|
3432
3897
|
|
|
3433
3898
|
//#endregion
|
|
@@ -3495,7 +3960,7 @@ const handleDocumentSymbol = (input) => {
|
|
|
3495
3960
|
const symbols = [];
|
|
3496
3961
|
for (const template of templates) {
|
|
3497
3962
|
const reconstructed = reconstructGraphql(template);
|
|
3498
|
-
const headerLen = reconstructed
|
|
3963
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
3499
3964
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
3500
3965
|
const outline = getOutline(preprocessed);
|
|
3501
3966
|
if (!outline) {
|
|
@@ -3516,6 +3981,11 @@ const handleDocumentSymbol = (input) => {
|
|
|
3516
3981
|
for (const tree of outline.outlineTrees) {
|
|
3517
3982
|
const symbol = convertTree(tree, toContentPos, mapper);
|
|
3518
3983
|
if (symbol) {
|
|
3984
|
+
if (template.kind === "fragment" && template.typeName) {
|
|
3985
|
+
symbol.detail = `on ${template.typeName}`;
|
|
3986
|
+
} else if (template.kind !== "fragment") {
|
|
3987
|
+
symbol.detail = template.kind;
|
|
3988
|
+
}
|
|
3519
3989
|
symbols.push(symbol);
|
|
3520
3990
|
}
|
|
3521
3991
|
}
|
|
@@ -3523,6 +3993,129 @@ const handleDocumentSymbol = (input) => {
|
|
|
3523
3993
|
return symbols;
|
|
3524
3994
|
};
|
|
3525
3995
|
|
|
3996
|
+
//#endregion
|
|
3997
|
+
//#region packages/lsp/src/handlers/field-tree-completion.ts
|
|
3998
|
+
/** Handle a completion request for a callback builder field tree node. */
|
|
3999
|
+
const handleFieldTreeCompletion = (input) => {
|
|
4000
|
+
const { fieldTree, schema, tsSource, offset } = input;
|
|
4001
|
+
const result = findNodeAtOffset(fieldTree, offset);
|
|
4002
|
+
if (!result) return [];
|
|
4003
|
+
if (result.kind === "field") {
|
|
4004
|
+
const { node } = result;
|
|
4005
|
+
const prefix = tsSource.slice(node.fieldNameSpan.start, offset);
|
|
4006
|
+
const parentType = schema.getType(node.parentTypeName);
|
|
4007
|
+
if (!parentType || !isObjectType(parentType)) return [];
|
|
4008
|
+
const fields = parentType.getFields();
|
|
4009
|
+
const items = [];
|
|
4010
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
4011
|
+
if (!name.startsWith(prefix)) continue;
|
|
4012
|
+
const namedType = getNamedType(field.type);
|
|
4013
|
+
items.push({
|
|
4014
|
+
label: name,
|
|
4015
|
+
kind: CompletionItemKind.Field,
|
|
4016
|
+
detail: namedType?.name
|
|
4017
|
+
});
|
|
4018
|
+
}
|
|
4019
|
+
return items;
|
|
4020
|
+
}
|
|
4021
|
+
if (result.kind === "unionMember") {
|
|
4022
|
+
const { parentNode } = result;
|
|
4023
|
+
const fieldType = parentNode.fieldTypeName ? schema.getType(parentNode.fieldTypeName) : null;
|
|
4024
|
+
if (!fieldType || !(fieldType instanceof GraphQLUnionType)) return [];
|
|
4025
|
+
const prefix = tsSource.slice(result.branch.typeNameSpan.start, offset);
|
|
4026
|
+
return fieldType.getTypes().filter((member) => member.name.startsWith(prefix)).map((member) => ({
|
|
4027
|
+
label: member.name,
|
|
4028
|
+
kind: CompletionItemKind.Class,
|
|
4029
|
+
detail: `member of ${parentNode.fieldTypeName}`
|
|
4030
|
+
}));
|
|
4031
|
+
}
|
|
4032
|
+
return [];
|
|
4033
|
+
};
|
|
4034
|
+
|
|
4035
|
+
//#endregion
|
|
4036
|
+
//#region packages/lsp/src/handlers/field-tree-definition.ts
|
|
4037
|
+
/** Handle a definition request for a callback builder field tree. */
|
|
4038
|
+
const handleFieldTreeDefinition = async (input) => {
|
|
4039
|
+
const { fieldTree, offset, schemaFiles } = input;
|
|
4040
|
+
const result = findNodeAtOffset(fieldTree, offset);
|
|
4041
|
+
if (!result) return [];
|
|
4042
|
+
const objectTypeInfos = buildObjectTypeInfos(schemaFiles);
|
|
4043
|
+
if (result.kind === "field") {
|
|
4044
|
+
const { node } = result;
|
|
4045
|
+
try {
|
|
4046
|
+
const defResult = await getDefinitionQueryResultForField(node.fieldName, node.parentTypeName, objectTypeInfos);
|
|
4047
|
+
return defResult.definitions.map((def) => ({
|
|
4048
|
+
uri: def.path ?? "",
|
|
4049
|
+
range: {
|
|
4050
|
+
start: {
|
|
4051
|
+
line: def.position.line,
|
|
4052
|
+
character: def.position.character
|
|
4053
|
+
},
|
|
4054
|
+
end: {
|
|
4055
|
+
line: def.range?.end?.line ?? def.position.line,
|
|
4056
|
+
character: def.range?.end?.character ?? def.position.character
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
}));
|
|
4060
|
+
} catch {
|
|
4061
|
+
return [];
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
if (result.kind === "unionMember") {
|
|
4065
|
+
const typeNameLen = result.branch.typeName.length;
|
|
4066
|
+
const dummyLoc = {
|
|
4067
|
+
start: 0,
|
|
4068
|
+
end: typeNameLen,
|
|
4069
|
+
startToken: null,
|
|
4070
|
+
endToken: null,
|
|
4071
|
+
source: null
|
|
4072
|
+
};
|
|
4073
|
+
const namedTypeNode = {
|
|
4074
|
+
kind: Kind.NAMED_TYPE,
|
|
4075
|
+
name: {
|
|
4076
|
+
kind: Kind.NAME,
|
|
4077
|
+
value: result.branch.typeName,
|
|
4078
|
+
loc: dummyLoc
|
|
4079
|
+
},
|
|
4080
|
+
loc: dummyLoc
|
|
4081
|
+
};
|
|
4082
|
+
try {
|
|
4083
|
+
const defResult = await getDefinitionQueryResultForNamedType("", namedTypeNode, objectTypeInfos);
|
|
4084
|
+
return defResult.definitions.map((def) => ({
|
|
4085
|
+
uri: def.path ?? "",
|
|
4086
|
+
range: {
|
|
4087
|
+
start: {
|
|
4088
|
+
line: def.position.line,
|
|
4089
|
+
character: def.position.character
|
|
4090
|
+
},
|
|
4091
|
+
end: {
|
|
4092
|
+
line: def.range?.end?.line ?? def.position.line,
|
|
4093
|
+
character: def.range?.end?.character ?? def.position.character
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}));
|
|
4097
|
+
} catch {
|
|
4098
|
+
return [];
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
return [];
|
|
4102
|
+
};
|
|
4103
|
+
|
|
4104
|
+
//#endregion
|
|
4105
|
+
//#region packages/lsp/src/handlers/field-tree-hover.ts
|
|
4106
|
+
/** Handle a hover request for a callback builder field tree node. */
|
|
4107
|
+
const handleFieldTreeHover = (input) => {
|
|
4108
|
+
const { fieldTree, offset } = input;
|
|
4109
|
+
const result = findNodeAtOffset(fieldTree, offset);
|
|
4110
|
+
if (!result || result.kind !== "field") return null;
|
|
4111
|
+
const { node } = result;
|
|
4112
|
+
if (!node.fieldTypeName) return null;
|
|
4113
|
+
return { contents: {
|
|
4114
|
+
kind: "markdown",
|
|
4115
|
+
value: `${node.fieldName}: ${node.fieldTypeName}`
|
|
4116
|
+
} };
|
|
4117
|
+
};
|
|
4118
|
+
|
|
3526
4119
|
//#endregion
|
|
3527
4120
|
//#region packages/lsp/src/handlers/formatting.ts
|
|
3528
4121
|
/**
|
|
@@ -3554,7 +4147,7 @@ const handleFormatting = (input) => {
|
|
|
3554
4147
|
const handleHover = (input) => {
|
|
3555
4148
|
const { template, schema, tsSource, tsPosition } = input;
|
|
3556
4149
|
const reconstructed = reconstructGraphql(template);
|
|
3557
|
-
const headerLen = reconstructed
|
|
4150
|
+
const headerLen = computeHeaderLen(template, reconstructed);
|
|
3558
4151
|
const { preprocessed } = preprocessFragmentArgs(reconstructed);
|
|
3559
4152
|
const mapper = createPositionMapper({
|
|
3560
4153
|
tsSource,
|
|
@@ -3718,6 +4311,115 @@ const handleRename = (input) => {
|
|
|
3718
4311
|
* LSP server: wires all components together via vscode-languageserver.
|
|
3719
4312
|
* @module
|
|
3720
4313
|
*/
|
|
4314
|
+
/**
|
|
4315
|
+
* Shared 3-phase dispatch: registry/ctx/doc guard → field-tree → template.
|
|
4316
|
+
* Used by completion, hover, and definition handlers.
|
|
4317
|
+
*/
|
|
4318
|
+
const resolvePositionContext = (registry, documents, uri, position) => {
|
|
4319
|
+
if (!registry) {
|
|
4320
|
+
return undefined;
|
|
4321
|
+
}
|
|
4322
|
+
const ctx = registry.resolveForUri(uri);
|
|
4323
|
+
if (!ctx) {
|
|
4324
|
+
return undefined;
|
|
4325
|
+
}
|
|
4326
|
+
const doc = documents.get(uri);
|
|
4327
|
+
if (!doc) {
|
|
4328
|
+
return undefined;
|
|
4329
|
+
}
|
|
4330
|
+
const tsSource = doc.getText();
|
|
4331
|
+
const offset = positionToOffset(tsSource, position);
|
|
4332
|
+
const tsPosition = {
|
|
4333
|
+
line: position.line,
|
|
4334
|
+
character: position.character
|
|
4335
|
+
};
|
|
4336
|
+
const untypedTree = ctx.documentManager.findFieldTreeAtOffset(uri, offset);
|
|
4337
|
+
if (untypedTree) {
|
|
4338
|
+
const schemaEntry$1 = ctx.schemaResolver.getSchema(untypedTree.schemaName);
|
|
4339
|
+
if (schemaEntry$1) {
|
|
4340
|
+
const typedTree = resolveFieldTree(untypedTree, schemaEntry$1.schema);
|
|
4341
|
+
if (typedTree) {
|
|
4342
|
+
return {
|
|
4343
|
+
kind: "fieldTree",
|
|
4344
|
+
ctx,
|
|
4345
|
+
tsSource,
|
|
4346
|
+
offset,
|
|
4347
|
+
tsPosition,
|
|
4348
|
+
typedTree,
|
|
4349
|
+
schema: schemaEntry$1.schema,
|
|
4350
|
+
schemaEntry: schemaEntry$1
|
|
4351
|
+
};
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
const template = ctx.documentManager.findTemplateAtOffset(uri, offset);
|
|
4356
|
+
if (!template) {
|
|
4357
|
+
return undefined;
|
|
4358
|
+
}
|
|
4359
|
+
const schemaEntry = ctx.schemaResolver.getSchema(template.schemaName);
|
|
4360
|
+
return {
|
|
4361
|
+
kind: "template",
|
|
4362
|
+
ctx,
|
|
4363
|
+
tsSource,
|
|
4364
|
+
offset,
|
|
4365
|
+
tsPosition,
|
|
4366
|
+
template,
|
|
4367
|
+
schemaEntry
|
|
4368
|
+
};
|
|
4369
|
+
};
|
|
4370
|
+
/** Server capabilities shared between direct and proxy initialization. */
|
|
4371
|
+
const serverCapabilities = {
|
|
4372
|
+
textDocumentSync: TextDocumentSyncKind.Full,
|
|
4373
|
+
hoverProvider: true,
|
|
4374
|
+
documentSymbolProvider: true,
|
|
4375
|
+
definitionProvider: true,
|
|
4376
|
+
referencesProvider: true,
|
|
4377
|
+
renameProvider: { prepareProvider: true },
|
|
4378
|
+
documentFormattingProvider: true,
|
|
4379
|
+
completionProvider: { triggerCharacters: [
|
|
4380
|
+
"{",
|
|
4381
|
+
"(",
|
|
4382
|
+
":",
|
|
4383
|
+
"@",
|
|
4384
|
+
"$",
|
|
4385
|
+
" ",
|
|
4386
|
+
"\n",
|
|
4387
|
+
".",
|
|
4388
|
+
"\""
|
|
4389
|
+
] },
|
|
4390
|
+
codeActionProvider: { codeActionKinds: ["refactor.extract"] }
|
|
4391
|
+
};
|
|
4392
|
+
/** Initialize the LSP server from InitializeParams. Used by both direct and proxy modes. */
|
|
4393
|
+
const initializeFromParams = (params, connection) => {
|
|
4394
|
+
const roots = resolveWorkspaceRoots(params);
|
|
4395
|
+
if (roots.length === 0) {
|
|
4396
|
+
connection.window.showErrorMessage("soda-gql LSP: no workspace root provided");
|
|
4397
|
+
return {
|
|
4398
|
+
result: { capabilities: {} },
|
|
4399
|
+
registry: undefined
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
const configPaths = discoverConfigs(roots);
|
|
4403
|
+
if (configPaths.length === 0) {
|
|
4404
|
+
connection.window.showErrorMessage("soda-gql LSP: no config file found");
|
|
4405
|
+
return {
|
|
4406
|
+
result: { capabilities: {} },
|
|
4407
|
+
registry: undefined
|
|
4408
|
+
};
|
|
4409
|
+
}
|
|
4410
|
+
const registryResult = createConfigRegistry(configPaths);
|
|
4411
|
+
if (registryResult.isErr()) {
|
|
4412
|
+
connection.window.showErrorMessage(`soda-gql LSP: ${registryResult.error.message}`);
|
|
4413
|
+
return {
|
|
4414
|
+
result: { capabilities: {} },
|
|
4415
|
+
registry: undefined
|
|
4416
|
+
};
|
|
4417
|
+
}
|
|
4418
|
+
return {
|
|
4419
|
+
result: { capabilities: serverCapabilities },
|
|
4420
|
+
registry: registryResult.value
|
|
4421
|
+
};
|
|
4422
|
+
};
|
|
3721
4423
|
const createLspServer = (options) => {
|
|
3722
4424
|
const connection = options?.connection ?? createConnection(ProposedFeatures.all);
|
|
3723
4425
|
const documents = new TextDocuments(TextDocument);
|
|
@@ -3743,22 +4445,10 @@ const createLspServer = (options) => {
|
|
|
3743
4445
|
});
|
|
3744
4446
|
return;
|
|
3745
4447
|
}
|
|
3746
|
-
const
|
|
3747
|
-
const entry = ctx.schemaResolver.getSchema(template.schemaName);
|
|
3748
|
-
if (!entry) {
|
|
3749
|
-
return [];
|
|
3750
|
-
}
|
|
3751
|
-
const externalFragments = ctx.documentManager.getExternalFragments(uri, template.schemaName).map((f) => f.definition);
|
|
3752
|
-
return [...computeTemplateDiagnostics({
|
|
3753
|
-
template,
|
|
3754
|
-
schema: entry.schema,
|
|
3755
|
-
tsSource: state.source,
|
|
3756
|
-
externalFragments
|
|
3757
|
-
})];
|
|
3758
|
-
});
|
|
4448
|
+
const diagnostics = collectRawDiagnostics(state, ctx);
|
|
3759
4449
|
connection.sendDiagnostics({
|
|
3760
4450
|
uri,
|
|
3761
|
-
diagnostics:
|
|
4451
|
+
diagnostics: [...diagnostics]
|
|
3762
4452
|
});
|
|
3763
4453
|
};
|
|
3764
4454
|
const publishDiagnosticsForAllOpen = () => {
|
|
@@ -3766,46 +4456,20 @@ const createLspServer = (options) => {
|
|
|
3766
4456
|
publishDiagnosticsForDocument(doc.uri);
|
|
3767
4457
|
}
|
|
3768
4458
|
};
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
return
|
|
3779
|
-
}
|
|
3780
|
-
|
|
3781
|
-
if (registryResult.isErr()) {
|
|
3782
|
-
connection.window.showErrorMessage(`soda-gql LSP: ${registryResult.error.message}`);
|
|
3783
|
-
return { capabilities: {} };
|
|
3784
|
-
}
|
|
3785
|
-
registry = registryResult.value;
|
|
3786
|
-
return { capabilities: {
|
|
3787
|
-
textDocumentSync: TextDocumentSyncKind.Full,
|
|
3788
|
-
hoverProvider: true,
|
|
3789
|
-
documentSymbolProvider: true,
|
|
3790
|
-
definitionProvider: true,
|
|
3791
|
-
referencesProvider: true,
|
|
3792
|
-
renameProvider: { prepareProvider: true },
|
|
3793
|
-
documentFormattingProvider: true,
|
|
3794
|
-
completionProvider: { triggerCharacters: [
|
|
3795
|
-
"{",
|
|
3796
|
-
"(",
|
|
3797
|
-
":",
|
|
3798
|
-
"@",
|
|
3799
|
-
"$",
|
|
3800
|
-
" ",
|
|
3801
|
-
"\n",
|
|
3802
|
-
"."
|
|
3803
|
-
] },
|
|
3804
|
-
codeActionProvider: { codeActionKinds: ["refactor.extract"] }
|
|
3805
|
-
} };
|
|
3806
|
-
});
|
|
4459
|
+
let initializeResult;
|
|
4460
|
+
if (options?.initializeParams) {
|
|
4461
|
+
const init = initializeFromParams(options.initializeParams, connection);
|
|
4462
|
+
registry = init.registry;
|
|
4463
|
+
initializeResult = init.result;
|
|
4464
|
+
} else {
|
|
4465
|
+
connection.onInitialize((params) => {
|
|
4466
|
+
const init = initializeFromParams(params, connection);
|
|
4467
|
+
registry = init.registry;
|
|
4468
|
+
return init.result;
|
|
4469
|
+
});
|
|
4470
|
+
}
|
|
3807
4471
|
connection.onInitialized(() => {
|
|
3808
|
-
connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [{ globPattern: "**/*.graphql" }] });
|
|
4472
|
+
connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [{ globPattern: "**/*.graphql" }, { globPattern: "**/soda-gql.config.*" }] });
|
|
3809
4473
|
});
|
|
3810
4474
|
documents.onDidChangeContent((change) => {
|
|
3811
4475
|
if (!registry) {
|
|
@@ -3833,95 +4497,97 @@ const createLspServer = (options) => {
|
|
|
3833
4497
|
});
|
|
3834
4498
|
});
|
|
3835
4499
|
connection.onCompletion((params) => {
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
}
|
|
3839
|
-
const ctx = registry.resolveForUri(params.textDocument.uri);
|
|
3840
|
-
if (!ctx) {
|
|
3841
|
-
return [];
|
|
3842
|
-
}
|
|
3843
|
-
const template = ctx.documentManager.findTemplateAtOffset(params.textDocument.uri, positionToOffset(documents.get(params.textDocument.uri)?.getText() ?? "", params.position));
|
|
3844
|
-
if (!template) {
|
|
4500
|
+
const resolved = resolvePositionContext(registry, documents, params.textDocument.uri, params.position);
|
|
4501
|
+
if (!resolved) {
|
|
3845
4502
|
return [];
|
|
3846
4503
|
}
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
4504
|
+
if (resolved.kind === "fieldTree") {
|
|
4505
|
+
return handleFieldTreeCompletion({
|
|
4506
|
+
fieldTree: resolved.typedTree,
|
|
4507
|
+
schema: resolved.schema,
|
|
4508
|
+
tsSource: resolved.tsSource,
|
|
4509
|
+
tsPosition: resolved.tsPosition,
|
|
4510
|
+
offset: resolved.offset
|
|
4511
|
+
});
|
|
3850
4512
|
}
|
|
3851
|
-
|
|
3852
|
-
if (!doc) {
|
|
4513
|
+
if (!resolved.schemaEntry) {
|
|
3853
4514
|
return [];
|
|
3854
4515
|
}
|
|
3855
|
-
const externalFragments = ctx.documentManager.getExternalFragments(params.textDocument.uri, template.schemaName).map((f) => f.definition);
|
|
4516
|
+
const externalFragments = resolved.ctx.documentManager.getExternalFragments(params.textDocument.uri, resolved.template.schemaName).map((f) => f.definition);
|
|
3856
4517
|
return handleCompletion({
|
|
3857
|
-
template,
|
|
3858
|
-
schema:
|
|
3859
|
-
tsSource:
|
|
3860
|
-
tsPosition:
|
|
3861
|
-
line: params.position.line,
|
|
3862
|
-
character: params.position.character
|
|
3863
|
-
},
|
|
4518
|
+
template: resolved.template,
|
|
4519
|
+
schema: resolved.schemaEntry.schema,
|
|
4520
|
+
tsSource: resolved.tsSource,
|
|
4521
|
+
tsPosition: resolved.tsPosition,
|
|
3864
4522
|
externalFragments
|
|
3865
4523
|
});
|
|
3866
4524
|
});
|
|
3867
4525
|
connection.onHover((params) => {
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
}
|
|
3871
|
-
const ctx = registry.resolveForUri(params.textDocument.uri);
|
|
3872
|
-
if (!ctx) {
|
|
3873
|
-
return null;
|
|
3874
|
-
}
|
|
3875
|
-
const doc = documents.get(params.textDocument.uri);
|
|
3876
|
-
if (!doc) {
|
|
4526
|
+
const resolved = resolvePositionContext(registry, documents, params.textDocument.uri, params.position);
|
|
4527
|
+
if (!resolved) {
|
|
3877
4528
|
return null;
|
|
3878
4529
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
4530
|
+
if (resolved.kind === "fieldTree") {
|
|
4531
|
+
return handleFieldTreeHover({
|
|
4532
|
+
fieldTree: resolved.typedTree,
|
|
4533
|
+
offset: resolved.offset
|
|
4534
|
+
});
|
|
3882
4535
|
}
|
|
3883
|
-
|
|
3884
|
-
if (!entry) {
|
|
4536
|
+
if (!resolved.schemaEntry) {
|
|
3885
4537
|
return null;
|
|
3886
4538
|
}
|
|
3887
4539
|
return handleHover({
|
|
3888
|
-
template,
|
|
3889
|
-
schema:
|
|
3890
|
-
tsSource:
|
|
3891
|
-
tsPosition:
|
|
3892
|
-
line: params.position.line,
|
|
3893
|
-
character: params.position.character
|
|
3894
|
-
}
|
|
4540
|
+
template: resolved.template,
|
|
4541
|
+
schema: resolved.schemaEntry.schema,
|
|
4542
|
+
tsSource: resolved.tsSource,
|
|
4543
|
+
tsPosition: resolved.tsPosition
|
|
3895
4544
|
});
|
|
3896
4545
|
});
|
|
3897
4546
|
connection.onDefinition(async (params) => {
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
4547
|
+
const resolved = resolvePositionContext(registry, documents, params.textDocument.uri, params.position);
|
|
4548
|
+
if (!resolved) {
|
|
4549
|
+
if (!registry) {
|
|
4550
|
+
return [];
|
|
4551
|
+
}
|
|
4552
|
+
const ctx = registry.resolveForUri(params.textDocument.uri);
|
|
4553
|
+
if (!ctx) {
|
|
4554
|
+
return [];
|
|
4555
|
+
}
|
|
4556
|
+
const doc = documents.get(params.textDocument.uri);
|
|
4557
|
+
if (!doc) {
|
|
4558
|
+
return [];
|
|
4559
|
+
}
|
|
4560
|
+
const offset = positionToOffset(doc.getText(), params.position);
|
|
4561
|
+
const typeNameTemplate = ctx.documentManager.findTemplateByTypeNameOffset(params.textDocument.uri, offset);
|
|
4562
|
+
if (typeNameTemplate?.typeName) {
|
|
4563
|
+
const typeNameEntry = ctx.schemaResolver.getSchema(typeNameTemplate.schemaName);
|
|
4564
|
+
if (typeNameEntry?.files) {
|
|
4565
|
+
return resolveTypeNameToSchemaDefinition(typeNameTemplate.typeName, typeNameEntry.files);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
3907
4568
|
return [];
|
|
3908
4569
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4570
|
+
if (resolved.kind === "fieldTree") {
|
|
4571
|
+
if (!resolved.schemaEntry.files) {
|
|
4572
|
+
return [];
|
|
4573
|
+
}
|
|
4574
|
+
return handleFieldTreeDefinition({
|
|
4575
|
+
fieldTree: resolved.typedTree,
|
|
4576
|
+
schema: resolved.schema,
|
|
4577
|
+
tsSource: resolved.tsSource,
|
|
4578
|
+
tsPosition: resolved.tsPosition,
|
|
4579
|
+
offset: resolved.offset,
|
|
4580
|
+
schemaFiles: resolved.schemaEntry.files
|
|
4581
|
+
});
|
|
3912
4582
|
}
|
|
3913
|
-
const externalFragments = ctx.documentManager.getExternalFragments(params.textDocument.uri, template.schemaName);
|
|
3914
|
-
const entry = ctx.schemaResolver.getSchema(template.schemaName);
|
|
4583
|
+
const externalFragments = resolved.ctx.documentManager.getExternalFragments(params.textDocument.uri, resolved.template.schemaName);
|
|
3915
4584
|
return handleDefinition({
|
|
3916
|
-
template,
|
|
3917
|
-
tsSource:
|
|
3918
|
-
tsPosition:
|
|
3919
|
-
line: params.position.line,
|
|
3920
|
-
character: params.position.character
|
|
3921
|
-
},
|
|
4585
|
+
template: resolved.template,
|
|
4586
|
+
tsSource: resolved.tsSource,
|
|
4587
|
+
tsPosition: resolved.tsPosition,
|
|
3922
4588
|
externalFragments,
|
|
3923
|
-
schema:
|
|
3924
|
-
schemaFiles:
|
|
4589
|
+
schema: resolved.schemaEntry?.schema,
|
|
4590
|
+
schemaFiles: resolved.schemaEntry?.files
|
|
3925
4591
|
});
|
|
3926
4592
|
});
|
|
3927
4593
|
connection.onReferences((params) => {
|
|
@@ -4089,11 +4755,18 @@ const createLspServer = (options) => {
|
|
|
4089
4755
|
}
|
|
4090
4756
|
}
|
|
4091
4757
|
}
|
|
4758
|
+
const configChanged = _params.changes.some((change) => /soda-gql\.config\.[cm]?[jt]s$/.test(change.uri) && (change.type === FileChangeType.Changed || change.type === FileChangeType.Created));
|
|
4759
|
+
if (configChanged) {
|
|
4760
|
+
connection.window.showInformationMessage("soda-gql: config file changed. Restart the language server to apply.");
|
|
4761
|
+
}
|
|
4092
4762
|
});
|
|
4093
4763
|
documents.listen(connection);
|
|
4094
|
-
return {
|
|
4095
|
-
|
|
4096
|
-
|
|
4764
|
+
return {
|
|
4765
|
+
start: () => {
|
|
4766
|
+
connection.listen();
|
|
4767
|
+
},
|
|
4768
|
+
initializeResult
|
|
4769
|
+
};
|
|
4097
4770
|
};
|
|
4098
4771
|
/** Check if SWC is unavailable and show a one-time error notification. */
|
|
4099
4772
|
const checkSwcUnavailable = (swcUnavailable, state, showError) => {
|
|
@@ -4150,5 +4823,5 @@ const positionToOffset = (source, position) => {
|
|
|
4150
4823
|
};
|
|
4151
4824
|
|
|
4152
4825
|
//#endregion
|
|
4153
|
-
export {
|
|
4154
|
-
//# sourceMappingURL=server-
|
|
4826
|
+
export { collectRawDiagnostics as a, preprocessFragmentArgs as c, lspErrors as i, createConfigRegistry as n, createPositionMapper as o, createSchemaResolver as r, createDocumentManager as s, createLspServer as t };
|
|
4827
|
+
//# sourceMappingURL=server-wPCHK04O.mjs.map
|