@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.
@@ -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 { hashSchema } from "@soda-gql/tools/codegen";
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.length - template.content.length;
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 extractTemplates = (uri, source) => {
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
- return walkAndExtract(program, gqlIdentifiers, positionCtx);
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 = extractTemplates(uri, source);
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.length - template.content.length;
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/schema-resolver.ts
505
- /**
506
- * Schema resolver: maps schema names to GraphQLSchema objects.
507
- * @module
508
- */
509
- /** Wrap buildASTSchema (which throws) in a Result. */
510
- const safeBuildASTSchema = (schemaName, documentNode) => {
511
- try {
512
- return ok(buildASTSchema(documentNode));
513
- } catch (e) {
514
- return err(lspErrors.schemaBuildFailed(schemaName, e instanceof Error ? e.message : String(e), e));
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
- /** Create a schema resolver from config. Loads all schemas eagerly. */
548
- const createSchemaResolver = (config) => {
549
- const cache = new Map();
550
- for (const [name, schemaConfig] of Object.entries(config.schemas)) {
551
- const result = loadAndBuildSchema(name, schemaConfig.schema);
552
- if (result.isErr()) {
553
- return err(result.error);
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
- cache.set(name, result.value);
556
- }
557
- const resolver = {
558
- getSchema: (schemaName) => cache.get(schemaName),
559
- getSchemaNames: () => [...cache.keys()],
560
- reloadSchema: (schemaName) => {
561
- const schemaConfig = config.schemas[schemaName];
562
- if (!schemaConfig) {
563
- return err(lspErrors.schemaNotConfigured(schemaName));
564
- }
565
- const result = loadAndBuildSchema(schemaName, schemaConfig.schema);
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
- return ok(resolver);
586
- };
587
-
588
- //#endregion
589
- //#region packages/lsp/src/config-registry.ts
590
- /**
591
- * Config registry: maps document URIs to their nearest config context.
592
- * Supports multiple soda-gql configs in a monorepo workspace.
593
- * @module
594
- */
595
- const createConfigRegistry = (configPaths) => {
596
- const sortedPaths = [...configPaths].sort((a, b) => b.length - a.length);
597
- const contexts = new Map();
598
- for (const configPath of sortedPaths) {
599
- const configResult = loadConfig(configPath);
600
- if (configResult.isErr()) {
601
- return err(lspErrors.configLoadFailed(`Failed to load config ${configPath}: ${configResult.error.message}`, configResult.error));
602
- }
603
- const config = configResult.value;
604
- const helper = createGraphqlSystemIdentifyHelper(config);
605
- const resolverResult = createSchemaResolver(config);
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.length - template.content.length;
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
- * Definition handler: provides go-to-definition for fragment spreads and schema fields/types.
3238
- * @module
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
- /** Build ObjectTypeInfo[] from schema file info for graphql-language-service definition APIs. */
3241
- const buildObjectTypeInfos = (files) => {
3242
- const result = [];
3243
- for (const file of files) {
3244
- const doc = parse(file.content);
3245
- for (const def of doc.definitions) {
3246
- if (isTypeDefinitionNode(def)) {
3247
- result.push({
3248
- filePath: pathToFileURL(file.filePath).href,
3249
- content: file.content,
3250
- definition: def
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 result;
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.length - template.content.length;
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 Promise.resolve([]);
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 Promise.resolve([]);
3786
+ return [];
3359
3787
  }
3360
3788
  const parentTypeName = namedParentType.name;
3361
3789
  const objectTypeInfos = buildObjectTypeInfos(schemaFiles);
3362
- return getDefinitionQueryResultForField(fieldName, parentTypeName, objectTypeInfos).then((result) => result.definitions.map((def) => ({
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 Promise.resolve([]);
3805
+ return [];
3377
3806
  };
3378
-
3379
- //#endregion
3380
- //#region packages/lsp/src/handlers/diagnostics.ts
3381
- /** Compute LSP diagnostics for a single GraphQL template. */
3382
- const computeTemplateDiagnostics = (input) => {
3383
- const { template, schema, tsSource } = input;
3384
- const reconstructed = reconstructGraphql(template);
3385
- const headerLen = reconstructed.length - template.content.length;
3386
- const { preprocessed } = preprocessFragmentArgs(reconstructed);
3387
- const mapper = createPositionMapper({
3388
- tsSource,
3389
- contentStartOffset: template.contentRange.start,
3390
- graphqlContent: template.content
3391
- });
3392
- const gqlDiagnostics = getDiagnostics(preprocessed, schema, undefined, undefined, input.externalFragments);
3393
- const placeholderPattern = /__FRAG_SPREAD_\d+__/;
3394
- const reconstructedLineOffsets = computeLineOffsets(preprocessed);
3395
- const contentLineOffsets = computeLineOffsets(template.content);
3396
- const toContentPosition = (pos) => {
3397
- const offset = positionToOffset$1(reconstructedLineOffsets, pos);
3398
- const contentOffset = Math.max(0, offset - headerLen);
3399
- return offsetToPosition(contentLineOffsets, contentOffset);
3400
- };
3401
- return gqlDiagnostics.filter((diag) => {
3402
- if (placeholderPattern.test(diag.message)) {
3403
- return false;
3404
- }
3405
- const offset = positionToOffset$1(reconstructedLineOffsets, diag.range.start);
3406
- if (offset < headerLen) {
3407
- return false;
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
- return true;
3410
- }).map((diag) => {
3411
- const startContent = toContentPosition(diag.range.start);
3412
- const endContent = toContentPosition(diag.range.end);
3413
- const startTs = mapper.graphqlToTs(startContent);
3414
- const endTs = mapper.graphqlToTs(endContent);
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: startTs.line,
3419
- character: startTs.character
3884
+ line: def.position.line,
3885
+ character: def.position.character
3420
3886
  },
3421
3887
  end: {
3422
- line: endTs.line,
3423
- character: endTs.character
3888
+ line: def.range?.end?.line ?? def.position.line,
3889
+ character: def.range?.end?.character ?? def.position.character
3424
3890
  }
3425
- },
3426
- message: diag.message,
3427
- severity: diag.severity,
3428
- source: "soda-gql"
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.length - template.content.length;
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.length - template.content.length;
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 allDiagnostics = state.templates.flatMap((template) => {
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: allDiagnostics
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
- connection.onInitialize((params) => {
3770
- const roots = resolveWorkspaceRoots(params);
3771
- if (roots.length === 0) {
3772
- connection.window.showErrorMessage("soda-gql LSP: no workspace root provided");
3773
- return { capabilities: {} };
3774
- }
3775
- const configPaths = discoverConfigs(roots);
3776
- if (configPaths.length === 0) {
3777
- connection.window.showErrorMessage("soda-gql LSP: no config file found");
3778
- return { capabilities: {} };
3779
- }
3780
- const registryResult = createConfigRegistry(configPaths);
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
- if (!registry) {
3837
- return [];
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
- const entry = ctx.schemaResolver.getSchema(template.schemaName);
3848
- if (!entry) {
3849
- return [];
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
- const doc = documents.get(params.textDocument.uri);
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: entry.schema,
3859
- tsSource: doc.getText(),
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
- if (!registry) {
3869
- return null;
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
- const template = ctx.documentManager.findTemplateAtOffset(params.textDocument.uri, positionToOffset(doc.getText(), params.position));
3880
- if (!template) {
3881
- return null;
4530
+ if (resolved.kind === "fieldTree") {
4531
+ return handleFieldTreeHover({
4532
+ fieldTree: resolved.typedTree,
4533
+ offset: resolved.offset
4534
+ });
3882
4535
  }
3883
- const entry = ctx.schemaResolver.getSchema(template.schemaName);
3884
- if (!entry) {
4536
+ if (!resolved.schemaEntry) {
3885
4537
  return null;
3886
4538
  }
3887
4539
  return handleHover({
3888
- template,
3889
- schema: entry.schema,
3890
- tsSource: doc.getText(),
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
- if (!registry) {
3899
- return [];
3900
- }
3901
- const ctx = registry.resolveForUri(params.textDocument.uri);
3902
- if (!ctx) {
3903
- return [];
3904
- }
3905
- const doc = documents.get(params.textDocument.uri);
3906
- if (!doc) {
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
- const template = ctx.documentManager.findTemplateAtOffset(params.textDocument.uri, positionToOffset(doc.getText(), params.position));
3910
- if (!template) {
3911
- return [];
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: doc.getText(),
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: entry?.schema,
3924
- schemaFiles: entry?.files
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 { start: () => {
4095
- connection.listen();
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 { createDocumentManager as a, lspErrors as i, createSchemaResolver as n, preprocessFragmentArgs as o, createPositionMapper as r, createLspServer as t };
4154
- //# sourceMappingURL=server-CqOUHwDk.mjs.map
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