polen 0.10.0-next.21 → 0.10.0-next.23
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/build/api/builder/builder.d.ts +4 -11
- package/build/api/builder/builder.d.ts.map +1 -1
- package/build/api/builder/builder.js +4 -3
- package/build/api/builder/builder.js.map +1 -1
- package/build/api/config/configurator.d.ts +62 -11
- package/build/api/config/configurator.d.ts.map +1 -1
- package/build/api/config/configurator.js +9 -0
- package/build/api/config/configurator.js.map +1 -1
- package/build/api/config/merge.d.ts.map +1 -1
- package/build/api/config/merge.js +8 -0
- package/build/api/config/merge.js.map +1 -1
- package/build/api/vite/plugins/core.d.ts.map +1 -1
- package/build/api/vite/plugins/core.js +1 -0
- package/build/api/vite/plugins/core.js.map +1 -1
- package/build/cli/commands/build.js +11 -7
- package/build/cli/commands/build.js.map +1 -1
- package/build/project-data.d.ts +1 -0
- package/build/project-data.d.ts.map +1 -1
- package/build/sandbox.js +40 -17
- package/build/sandbox.js.map +1 -1
- package/build/template/components/CodeBlock.d.ts.map +1 -1
- package/build/template/components/CodeBlock.js +3 -5
- package/build/template/components/CodeBlock.js.map +1 -1
- package/build/template/components/Field.js +1 -1
- package/build/template/components/Field.js.map +1 -1
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts +31 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.js +275 -0
- package/build/template/components/GraphQLInteractive/GraphQLInteractive.js.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts +39 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js +51 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.js.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts +33 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js +242 -0
- package/build/template/components/GraphQLInteractive/components/GraphQLTokenPopover.js.map +1 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts +45 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js +176 -0
- package/build/template/components/GraphQLInteractive/hooks/use-popover-state.js.map +1 -0
- package/build/template/components/GraphQLInteractive/index.d.ts +2 -0
- package/build/template/components/GraphQLInteractive/index.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/index.js +2 -0
- package/build/template/components/GraphQLInteractive/index.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts +52 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js +34 -0
- package/build/template/components/GraphQLInteractive/lib/graphql-node-types.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/parser.d.ts +71 -0
- package/build/template/components/GraphQLInteractive/lib/parser.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/parser.js +836 -0
- package/build/template/components/GraphQLInteractive/lib/parser.js.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts +98 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.d.ts.map +1 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js +31 -0
- package/build/template/components/GraphQLInteractive/lib/semantic-nodes.js.map +1 -0
- package/build/template/components/content/$$.d.ts +0 -1
- package/build/template/components/content/$$.d.ts.map +1 -1
- package/build/template/components/content/$$.js +0 -1
- package/build/template/components/content/$$.js.map +1 -1
- package/package.json +5 -21
- package/src/api/builder/builder.ts +8 -13
- package/src/api/config/configurator.ts +72 -11
- package/src/api/config/merge.ts +13 -0
- package/src/api/vite/plugins/core.ts +1 -0
- package/src/cli/commands/build.ts +11 -7
- package/src/lib/kit-temp.test.ts +9 -9
- package/src/project-data.ts +1 -0
- package/src/sandbox.ts +40 -17
- package/src/template/components/CodeBlock.tsx +6 -9
- package/src/template/components/Field.tsx +1 -1
- package/src/template/components/GraphQLInteractive/GraphQLInteractive.tsx +464 -0
- package/src/template/components/GraphQLInteractive/components/GraphQLErrorBoundary.tsx +96 -0
- package/src/template/components/GraphQLInteractive/components/GraphQLTokenPopover.tsx +492 -0
- package/src/template/components/GraphQLInteractive/hooks/use-popover-state.ts +244 -0
- package/src/template/components/GraphQLInteractive/index.ts +1 -0
- package/src/template/components/GraphQLInteractive/lib/graphql-node-types.ts +217 -0
- package/src/template/components/GraphQLInteractive/lib/parser.ts +1075 -0
- package/src/template/components/GraphQLInteractive/lib/semantic-nodes.ts +154 -0
- package/src/template/components/GraphQLInteractive/tests/parser-comment.test.ts +33 -0
- package/src/template/components/GraphQLInteractive/tests/parser-error-hint.test.ts +102 -0
- package/src/template/components/GraphQLInteractive/tests/parser.test.ts +131 -0
- package/src/template/components/content/$$.ts +0 -1
- package/build/template/components/content/GraphQLDocumentWithSchema.d.ts +0 -8
- package/build/template/components/content/GraphQLDocumentWithSchema.d.ts.map +0 -1
- package/build/template/components/content/GraphQLDocumentWithSchema.js +0 -13
- package/build/template/components/content/GraphQLDocumentWithSchema.js.map +0 -1
- package/build/template/components/content/GraphQLDocumentWrapper.d.ts +0 -7
- package/build/template/components/content/GraphQLDocumentWrapper.d.ts.map +0 -1
- package/build/template/components/content/GraphQLDocumentWrapper.js +0 -48
- package/build/template/components/content/GraphQLDocumentWrapper.js.map +0 -1
- package/src/template/components/content/GraphQLDocumentWithSchema.tsx +0 -13
- package/src/template/components/content/GraphQLDocumentWrapper.tsx +0 -72
@@ -0,0 +1,1075 @@
|
|
1
|
+
/**
|
2
|
+
* Tree-sitter GraphQL parsing with semantic analysis
|
3
|
+
*
|
4
|
+
* This module combines tree-sitter syntax parsing with GraphQL semantic
|
5
|
+
* analysis to create unified tokens for interactive code blocks.
|
6
|
+
*/
|
7
|
+
|
8
|
+
import type { CodeAnnotation } from 'codehike/code'
|
9
|
+
import {
|
10
|
+
getNamedType,
|
11
|
+
type GraphQLArgument,
|
12
|
+
GraphQLEnumType,
|
13
|
+
type GraphQLField,
|
14
|
+
GraphQLInputObjectType,
|
15
|
+
GraphQLInterfaceType,
|
16
|
+
GraphQLObjectType,
|
17
|
+
type GraphQLOutputType,
|
18
|
+
GraphQLScalarType,
|
19
|
+
type GraphQLSchema,
|
20
|
+
GraphQLUnionType,
|
21
|
+
isInterfaceType,
|
22
|
+
isObjectType,
|
23
|
+
} from 'graphql'
|
24
|
+
import graphqlWasmUrl from 'tree-sitter-graphql-grammar-wasm/grammar.wasm?url'
|
25
|
+
import * as WebTreeSitter from 'web-tree-sitter'
|
26
|
+
import treeSitterWasmUrl from 'web-tree-sitter/web-tree-sitter.wasm?url'
|
27
|
+
import {
|
28
|
+
isKeywordNodeType,
|
29
|
+
isLiteralNodeType,
|
30
|
+
isPunctuationNodeType,
|
31
|
+
type TreeSitterGraphQLNodeType,
|
32
|
+
} from './graphql-node-types.js'
|
33
|
+
import type { SemanticNode } from './semantic-nodes.js'
|
34
|
+
import {
|
35
|
+
isArgument,
|
36
|
+
isFragment,
|
37
|
+
isInputField,
|
38
|
+
isInvalidField,
|
39
|
+
isOperation,
|
40
|
+
isOutputField,
|
41
|
+
isVariable,
|
42
|
+
} from './semantic-nodes.js'
|
43
|
+
|
44
|
+
/**
|
45
|
+
* Unified token structure that combines tree-sitter and GraphQL semantics
|
46
|
+
*
|
47
|
+
* This interface represents a single parsed token from a GraphQL document,
|
48
|
+
* enriched with semantic information from the GraphQL schema when available.
|
49
|
+
* It provides a consistent API for accessing syntax highlighting, interactivity,
|
50
|
+
* and CodeHike annotation data.
|
51
|
+
*
|
52
|
+
* @example
|
53
|
+
* ```typescript
|
54
|
+
* const tokens = await parseGraphQLWithTreeSitter(code, [], schema)
|
55
|
+
* const fieldToken = tokens.find(t => t.text === 'name')
|
56
|
+
*
|
57
|
+
* if (fieldToken?.polen.isInteractive()) {
|
58
|
+
* const url = fieldToken.polen.getReferenceUrl()
|
59
|
+
* console.log(`Navigate to: ${url}`)
|
60
|
+
* }
|
61
|
+
* ```
|
62
|
+
*/
|
63
|
+
export interface GraphQLToken {
|
64
|
+
/** Reference to the tree-sitter node that this token represents */
|
65
|
+
treeSitterNode: WebTreeSitter.Node
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Optional semantic information from GraphQL schema analysis
|
69
|
+
* This includes type information, field definitions, and validation results
|
70
|
+
*/
|
71
|
+
semantic?: SemanticNode
|
72
|
+
|
73
|
+
/** Text content of the token (computed from tree-sitter node) */
|
74
|
+
get text(): string
|
75
|
+
|
76
|
+
/** Start character position in the source code (computed from tree-sitter node) */
|
77
|
+
get start(): number
|
78
|
+
|
79
|
+
/** End character position in the source code (computed from tree-sitter node) */
|
80
|
+
get end(): number
|
81
|
+
|
82
|
+
/** Polen specific functionality for interactive GraphQL documentation */
|
83
|
+
polen: {
|
84
|
+
/** Check if this token should be interactive (clickable/hoverable) */
|
85
|
+
isInteractive: () => boolean
|
86
|
+
/** Get the reference URL for navigation, or null if not applicable */
|
87
|
+
getReferenceUrl: () => string | null
|
88
|
+
}
|
89
|
+
|
90
|
+
/** Syntax highlighting functionality */
|
91
|
+
highlighter: {
|
92
|
+
/** Get the CSS class name for styling this token */
|
93
|
+
getCssClass: () => string
|
94
|
+
}
|
95
|
+
|
96
|
+
/** CodeHike integration for enhanced code block features */
|
97
|
+
codeHike: {
|
98
|
+
/** Array of CodeHike annotations that apply to this token */
|
99
|
+
annotations: CodeAnnotation[]
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
/**
|
104
|
+
* Implementation of unified token
|
105
|
+
*/
|
106
|
+
class UnifiedToken implements GraphQLToken {
|
107
|
+
public polen: { isInteractive: () => boolean; getReferenceUrl: () => string | null }
|
108
|
+
public highlighter: { getCssClass: () => string }
|
109
|
+
public codeHike: { annotations: CodeAnnotation[] }
|
110
|
+
|
111
|
+
// Cache these values to avoid WASM access issues
|
112
|
+
private _text: string
|
113
|
+
private _start: number
|
114
|
+
private _end: number
|
115
|
+
private _nodeType: TreeSitterGraphQLNodeType
|
116
|
+
|
117
|
+
constructor(
|
118
|
+
public treeSitterNode: WebTreeSitter.Node,
|
119
|
+
public semantic: SemanticNode | undefined,
|
120
|
+
annotations: CodeAnnotation[],
|
121
|
+
) {
|
122
|
+
// Cache the values immediately to avoid WASM access issues later
|
123
|
+
// This works for both real WebTreeSitter nodes and synthetic nodes
|
124
|
+
this._text = treeSitterNode.text
|
125
|
+
this._start = treeSitterNode.startIndex
|
126
|
+
this._end = treeSitterNode.endIndex
|
127
|
+
this._nodeType = treeSitterNode.type as TreeSitterGraphQLNodeType
|
128
|
+
|
129
|
+
this.codeHike = { annotations }
|
130
|
+
|
131
|
+
// Polen namespace
|
132
|
+
this.polen = {
|
133
|
+
isInteractive: () => this._isInteractive(),
|
134
|
+
getReferenceUrl: () => this._getReferenceUrl(),
|
135
|
+
}
|
136
|
+
|
137
|
+
// Highlighter namespace
|
138
|
+
this.highlighter = {
|
139
|
+
getCssClass: () => this._getCssClass(),
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
get text(): string {
|
144
|
+
return this._text
|
145
|
+
}
|
146
|
+
|
147
|
+
get start(): number {
|
148
|
+
return this._start
|
149
|
+
}
|
150
|
+
|
151
|
+
get end(): number {
|
152
|
+
return this._end
|
153
|
+
}
|
154
|
+
|
155
|
+
private _getCssClass(): string {
|
156
|
+
const nodeType = this._nodeType
|
157
|
+
|
158
|
+
// Development-only validation
|
159
|
+
if (process.env['NODE_ENV'] === 'development') {
|
160
|
+
// Validate that the node type is actually a valid TreeSitterGraphQLNodeType
|
161
|
+
const validTypes = new Set([
|
162
|
+
// Add a few common types for validation
|
163
|
+
'document',
|
164
|
+
'name',
|
165
|
+
'field',
|
166
|
+
'argument',
|
167
|
+
'variable',
|
168
|
+
'comment',
|
169
|
+
'error_hint',
|
170
|
+
'whitespace',
|
171
|
+
'string_value',
|
172
|
+
'int_value',
|
173
|
+
'float_value',
|
174
|
+
'query',
|
175
|
+
'mutation',
|
176
|
+
'subscription',
|
177
|
+
])
|
178
|
+
|
179
|
+
if (!validTypes.has(nodeType as any) && !nodeType.match(/^[a-z_]+$/)) {
|
180
|
+
console.warn(`Unknown tree-sitter node type: "${nodeType}". Consider adding to TreeSitterGraphQLNodeType.`)
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
// Error hints
|
185
|
+
if (nodeType === 'error_hint') {
|
186
|
+
return 'graphql-error-hint'
|
187
|
+
}
|
188
|
+
|
189
|
+
// Comments
|
190
|
+
if (nodeType === 'comment' || nodeType === 'description') {
|
191
|
+
return 'graphql-comment'
|
192
|
+
}
|
193
|
+
|
194
|
+
// Keywords
|
195
|
+
if (isKeywordNodeType(nodeType)) {
|
196
|
+
return 'graphql-keyword'
|
197
|
+
}
|
198
|
+
|
199
|
+
// Literals
|
200
|
+
if (nodeType === 'string_value') return 'graphql-string'
|
201
|
+
if (nodeType === 'int_value' || nodeType === 'float_value') return 'graphql-number'
|
202
|
+
|
203
|
+
// Punctuation
|
204
|
+
if (isPunctuationNodeType(nodeType)) {
|
205
|
+
return 'graphql-punctuation'
|
206
|
+
}
|
207
|
+
|
208
|
+
// Names - use semantic info for better classification
|
209
|
+
if (nodeType === 'name') {
|
210
|
+
// Check if this is an invalid field (has invalidField semantic)
|
211
|
+
if (this.semantic && 'kind' in this.semantic && this.semantic.kind === 'InvalidField') {
|
212
|
+
return 'graphql-field-error'
|
213
|
+
}
|
214
|
+
|
215
|
+
if (isOutputField(this.semantic) || isInputField(this.semantic)) {
|
216
|
+
return 'graphql-field-interactive'
|
217
|
+
}
|
218
|
+
if (
|
219
|
+
this.semantic instanceof GraphQLObjectType
|
220
|
+
|| this.semantic instanceof GraphQLScalarType
|
221
|
+
|| this.semantic instanceof GraphQLInterfaceType
|
222
|
+
) {
|
223
|
+
return 'graphql-type-interactive'
|
224
|
+
}
|
225
|
+
if (isVariable(this.semantic)) {
|
226
|
+
return 'graphql-variable'
|
227
|
+
}
|
228
|
+
if (isOperation(this.semantic)) {
|
229
|
+
return 'graphql-operation'
|
230
|
+
}
|
231
|
+
if (isFragment(this.semantic)) {
|
232
|
+
return 'graphql-fragment'
|
233
|
+
}
|
234
|
+
if (isArgument(this.semantic)) {
|
235
|
+
return 'graphql-argument'
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
// Variables
|
240
|
+
if (nodeType === 'variable') return 'graphql-variable'
|
241
|
+
|
242
|
+
return 'graphql-text'
|
243
|
+
}
|
244
|
+
|
245
|
+
private _isInteractive(): boolean {
|
246
|
+
if (!this.semantic) return false
|
247
|
+
|
248
|
+
// Fields, type references, arguments, and invalid fields are interactive
|
249
|
+
return isOutputField(this.semantic)
|
250
|
+
|| isInputField(this.semantic)
|
251
|
+
|| isArgument(this.semantic)
|
252
|
+
|| isInvalidField(this.semantic) // Invalid fields should show error popovers
|
253
|
+
|| this.semantic instanceof GraphQLObjectType
|
254
|
+
|| this.semantic instanceof GraphQLScalarType
|
255
|
+
|| this.semantic instanceof GraphQLInterfaceType
|
256
|
+
|| this.semantic instanceof GraphQLUnionType
|
257
|
+
|| this.semantic instanceof GraphQLEnumType
|
258
|
+
|| this.semantic instanceof GraphQLInputObjectType
|
259
|
+
}
|
260
|
+
|
261
|
+
private _getReferenceUrl(): string | null {
|
262
|
+
if (!this.semantic) return null
|
263
|
+
|
264
|
+
// Arguments - use #<field>__<argument> pattern
|
265
|
+
if (isArgument(this.semantic)) {
|
266
|
+
return `/reference/${this.semantic.parentType.name}#${this.semantic.parentField.name}__${this.semantic.argumentDef.name}`
|
267
|
+
}
|
268
|
+
|
269
|
+
// Output fields - use hash links since field routes aren't connected yet
|
270
|
+
if (isOutputField(this.semantic)) {
|
271
|
+
return `/reference/${this.semantic.parentType.name}#${this.semantic.fieldDef.name}`
|
272
|
+
}
|
273
|
+
|
274
|
+
// Input fields - use hash links since field routes aren't connected yet
|
275
|
+
if (isInputField(this.semantic)) {
|
276
|
+
return `/reference/${this.semantic.parentType.name}#${this.semantic.fieldDef.name}`
|
277
|
+
}
|
278
|
+
|
279
|
+
// Type references - use :type pattern
|
280
|
+
if (this.semantic instanceof GraphQLObjectType) {
|
281
|
+
return `/reference/${this.semantic.name}`
|
282
|
+
}
|
283
|
+
|
284
|
+
if (this.semantic instanceof GraphQLScalarType) {
|
285
|
+
return `/reference/${this.semantic.name}`
|
286
|
+
}
|
287
|
+
|
288
|
+
if (this.semantic instanceof GraphQLInterfaceType) {
|
289
|
+
return `/reference/${this.semantic.name}`
|
290
|
+
}
|
291
|
+
|
292
|
+
if (this.semantic instanceof GraphQLUnionType) {
|
293
|
+
return `/reference/${this.semantic.name}`
|
294
|
+
}
|
295
|
+
|
296
|
+
if (this.semantic instanceof GraphQLEnumType) {
|
297
|
+
return `/reference/${this.semantic.name}`
|
298
|
+
}
|
299
|
+
|
300
|
+
if (this.semantic instanceof GraphQLInputObjectType) {
|
301
|
+
return `/reference/${this.semantic.name}`
|
302
|
+
}
|
303
|
+
|
304
|
+
return null
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
308
|
+
// Cache for the parser instance
|
309
|
+
let parserPromise: Promise<WebTreeSitter.Parser> | null = null
|
310
|
+
|
311
|
+
/**
|
312
|
+
* Minimal synthetic node that implements just enough of the WebTreeSitter.Node interface
|
313
|
+
* Uses TreeSitterGraphQLNodeType for type safety
|
314
|
+
*
|
315
|
+
* IMPORTANT: This must be a class with getters to match WebTreeSitter.Node's WASM interface.
|
316
|
+
* Plain objects with properties will cause "memory access out of bounds" errors when
|
317
|
+
* tree-sitter tries to call the WASM getter functions.
|
318
|
+
*/
|
319
|
+
class SyntheticNode {
|
320
|
+
constructor(
|
321
|
+
public type: TreeSitterGraphQLNodeType,
|
322
|
+
private _text: string,
|
323
|
+
private _startIndex: number,
|
324
|
+
private _endIndex: number,
|
325
|
+
) {}
|
326
|
+
|
327
|
+
// These getters match WebTreeSitter.Node's interface
|
328
|
+
get text(): string {
|
329
|
+
return this._text
|
330
|
+
}
|
331
|
+
|
332
|
+
get startIndex(): number {
|
333
|
+
return this._startIndex
|
334
|
+
}
|
335
|
+
|
336
|
+
get endIndex(): number {
|
337
|
+
return this._endIndex
|
338
|
+
}
|
339
|
+
|
340
|
+
get childCount(): number {
|
341
|
+
return 0
|
342
|
+
}
|
343
|
+
|
344
|
+
get parent(): null {
|
345
|
+
return null
|
346
|
+
}
|
347
|
+
}
|
348
|
+
|
349
|
+
/**
|
350
|
+
* Tracks semantic context while walking the tree-sitter AST
|
351
|
+
*
|
352
|
+
* This class maintains the current GraphQL execution context as we traverse
|
353
|
+
* the syntax tree, allowing us to resolve field references to their schema
|
354
|
+
* definitions and validate field access.
|
355
|
+
*
|
356
|
+
* ## Context Management Strategy
|
357
|
+
*
|
358
|
+
* The semantic context uses a stack-based approach to track the current type context
|
359
|
+
* as we traverse nested GraphQL selections. This is essential for resolving field
|
360
|
+
* references since field names are only meaningful within their parent type context.
|
361
|
+
*
|
362
|
+
* ### Type Stack Management:
|
363
|
+
* - Each stack entry contains: { type: GraphQLType, field?: GraphQLField }
|
364
|
+
* - The `field` property tracks the field that led us to this type level
|
365
|
+
* - Stack depth corresponds to GraphQL selection nesting depth
|
366
|
+
* - Root level: operation root type (Query/Mutation/Subscription)
|
367
|
+
* - Nested levels: field return types that support sub-selections
|
368
|
+
*
|
369
|
+
* ### Context Transitions:
|
370
|
+
* - `enterOperation()`: Sets root type based on operation type
|
371
|
+
* - `enterField()`: Pushes field's return type if it's selectable (Object/Interface)
|
372
|
+
* - `exitField()`: Pops from stack when leaving a field's selection set
|
373
|
+
* - `enterFragment()`: Switches context to fragment's target type
|
374
|
+
*
|
375
|
+
* ### Argument Resolution Challenge:
|
376
|
+
* Arguments appear in the AST before their parent field context is established,
|
377
|
+
* requiring the complex lookup logic documented in the argument parsing section.
|
378
|
+
*
|
379
|
+
* @example
|
380
|
+
* ```typescript
|
381
|
+
* const context = new SemanticContext(schema)
|
382
|
+
* context.enterOperation('query') // Stack: [Query]
|
383
|
+
* context.enterField('user') // Stack: [Query, User]
|
384
|
+
* const fieldInfo = context.getFieldInfo('name') // Gets User.name field
|
385
|
+
* context.exitField() // Stack: [Query]
|
386
|
+
* ```
|
387
|
+
*/
|
388
|
+
class SemanticContext {
|
389
|
+
/**
|
390
|
+
* Stack of type contexts representing the current selection path.
|
391
|
+
* Each entry tracks the type we're currently selecting from and optionally
|
392
|
+
* the field that brought us to this type level.
|
393
|
+
*/
|
394
|
+
private typeStack: Array<{ type: GraphQLObjectType | GraphQLInterfaceType; field?: GraphQLField<any, any> }> = []
|
395
|
+
|
396
|
+
/** Current operation type, set when entering an operation definition */
|
397
|
+
operationType: 'query' | 'mutation' | 'subscription' | null = null
|
398
|
+
|
399
|
+
/** GraphQL schema used for type lookups and validation */
|
400
|
+
schema: GraphQLSchema
|
401
|
+
|
402
|
+
constructor(schema: GraphQLSchema) {
|
403
|
+
this.schema = schema
|
404
|
+
}
|
405
|
+
|
406
|
+
enterOperation(type: string) {
|
407
|
+
this.operationType = type as 'query' | 'mutation' | 'subscription'
|
408
|
+
const rootType = type === 'query'
|
409
|
+
? this.schema.getQueryType()
|
410
|
+
: type === 'mutation'
|
411
|
+
? this.schema.getMutationType()
|
412
|
+
: type === 'subscription'
|
413
|
+
? this.schema.getSubscriptionType()
|
414
|
+
: null
|
415
|
+
|
416
|
+
if (rootType) {
|
417
|
+
this.typeStack = [{ type: rootType }]
|
418
|
+
}
|
419
|
+
}
|
420
|
+
|
421
|
+
enterFragment(typeName: string) {
|
422
|
+
const type = this.schema.getType(typeName)
|
423
|
+
|
424
|
+
if (type && (isObjectType(type) || isInterfaceType(type))) {
|
425
|
+
this.typeStack = [{ type }]
|
426
|
+
}
|
427
|
+
}
|
428
|
+
|
429
|
+
getFieldInfo(
|
430
|
+
fieldName: string,
|
431
|
+
): { parentType: GraphQLObjectType | GraphQLInterfaceType; fieldDef: GraphQLField<any, any> } | null {
|
432
|
+
const current = this.typeStack[this.typeStack.length - 1]
|
433
|
+
if (!current) return null
|
434
|
+
|
435
|
+
const fields = current.type.getFields()
|
436
|
+
const fieldDef = fields[fieldName]
|
437
|
+
|
438
|
+
if (fieldDef) {
|
439
|
+
return { parentType: current.type, fieldDef }
|
440
|
+
}
|
441
|
+
return null
|
442
|
+
}
|
443
|
+
|
444
|
+
enterField(fieldName: string) {
|
445
|
+
const fieldInfo = this.getFieldInfo(fieldName)
|
446
|
+
if (fieldInfo) {
|
447
|
+
// Only push to stack if field type is object/interface
|
448
|
+
const fieldType = getNamedType(fieldInfo.fieldDef.type)
|
449
|
+
if (isObjectType(fieldType) || isInterfaceType(fieldType)) {
|
450
|
+
// Push new context with the field that brought us here
|
451
|
+
this.typeStack.push({ type: fieldType, field: fieldInfo.fieldDef })
|
452
|
+
}
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
exitField() {
|
457
|
+
// Only pop if we're not at root and the last entry has a field
|
458
|
+
// (meaning it was pushed by enterField for an object/interface type)
|
459
|
+
if (this.typeStack.length > 1) {
|
460
|
+
const last = this.typeStack[this.typeStack.length - 1]
|
461
|
+
if (last && last.field) {
|
462
|
+
this.typeStack.pop()
|
463
|
+
}
|
464
|
+
}
|
465
|
+
}
|
466
|
+
|
467
|
+
getArgumentInfo(argName: string) {
|
468
|
+
const current = this.typeStack[this.typeStack.length - 1]
|
469
|
+
if (!current?.field) return null
|
470
|
+
|
471
|
+
const arg = current.field.args.find(a => a.name === argName)
|
472
|
+
return arg ? { field: current.field, arg, parentType: current.type } : null
|
473
|
+
}
|
474
|
+
|
475
|
+
getCurrentType(): (GraphQLObjectType | GraphQLInterfaceType) | null {
|
476
|
+
const current = this.typeStack[this.typeStack.length - 1]
|
477
|
+
return current?.type || null
|
478
|
+
}
|
479
|
+
|
480
|
+
lookupType(typeName: string) {
|
481
|
+
return this.schema.getType(typeName)
|
482
|
+
}
|
483
|
+
|
484
|
+
reset() {
|
485
|
+
this.typeStack = []
|
486
|
+
this.operationType = null
|
487
|
+
}
|
488
|
+
}
|
489
|
+
|
490
|
+
/**
|
491
|
+
* Parse GraphQL code into interactive tokens with semantic information
|
492
|
+
*
|
493
|
+
* @param code - The raw GraphQL code to parse
|
494
|
+
* @param annotations - CodeHike annotations that might affect rendering
|
495
|
+
* @param schema - Optional GraphQL schema for semantic analysis
|
496
|
+
* @returns Array of tokens representing the parsed code
|
497
|
+
*/
|
498
|
+
export async function parseGraphQLWithTreeSitter(
|
499
|
+
code: string,
|
500
|
+
annotations: CodeAnnotation[] = [],
|
501
|
+
schema?: GraphQLSchema,
|
502
|
+
): Promise<GraphQLToken[]> {
|
503
|
+
// Validate input
|
504
|
+
if (!code || typeof code !== 'string') {
|
505
|
+
throw new Error('Invalid GraphQL code: code must be a non-empty string')
|
506
|
+
}
|
507
|
+
|
508
|
+
// Prevent parsing extremely large documents that could cause performance issues
|
509
|
+
if (code.length > 100_000) {
|
510
|
+
throw new Error('GraphQL document too large: maximum 100,000 characters allowed')
|
511
|
+
}
|
512
|
+
|
513
|
+
// Step 1: Parse with tree-sitter
|
514
|
+
const parser = await getParser()
|
515
|
+
const tree = parser.parse(code)
|
516
|
+
|
517
|
+
if (!tree) {
|
518
|
+
throw new Error('Tree-sitter failed to parse GraphQL code')
|
519
|
+
}
|
520
|
+
|
521
|
+
// Check if tree-sitter found syntax errors (disabled for now as it may be too strict)
|
522
|
+
// if (tree.rootNode.hasError) {
|
523
|
+
// throw new Error('GraphQL syntax error detected by tree-sitter parser')
|
524
|
+
// }
|
525
|
+
|
526
|
+
try {
|
527
|
+
// Step 2: Walk tree and attach semantics
|
528
|
+
const tokens = collectTokensWithSemantics(tree, code, schema, annotations)
|
529
|
+
|
530
|
+
// Step 3: Add error hint tokens after invalid fields
|
531
|
+
const tokensWithHints = addErrorHintTokens(tokens, code, annotations)
|
532
|
+
|
533
|
+
return tokensWithHints
|
534
|
+
} finally {
|
535
|
+
// ## Tree-sitter Resource Lifecycle Management
|
536
|
+
//
|
537
|
+
// Tree-sitter creates native WASM objects that must be explicitly freed to prevent memory leaks.
|
538
|
+
// The tree object holds references to parsed nodes and internal parser state that won't be
|
539
|
+
// garbage collected automatically by JavaScript.
|
540
|
+
//
|
541
|
+
// Critical cleanup points:
|
542
|
+
// 1. Always call tree.delete() in a finally block to ensure cleanup even on errors
|
543
|
+
// 2. Do not access tree or any of its nodes after calling delete()
|
544
|
+
// 3. The parser instance is cached globally and reused across multiple parsing calls
|
545
|
+
//
|
546
|
+
// Memory safety: Once tree.delete() is called, all WebTreeSitter.Node references become invalid.
|
547
|
+
// Our tokens hold references to these nodes, but only use their text and position properties
|
548
|
+
// which are copied during token creation, so the nodes can be safely deleted.
|
549
|
+
tree.delete()
|
550
|
+
}
|
551
|
+
}
|
552
|
+
|
553
|
+
/**
|
554
|
+
* Get or create the tree-sitter parser instance
|
555
|
+
*/
|
556
|
+
async function getParser(): Promise<WebTreeSitter.Parser> {
|
557
|
+
if (!parserPromise) {
|
558
|
+
parserPromise = initializeTreeSitter()
|
559
|
+
}
|
560
|
+
return parserPromise
|
561
|
+
}
|
562
|
+
|
563
|
+
/**
|
564
|
+
* Initialize tree-sitter with the GraphQL grammar
|
565
|
+
*/
|
566
|
+
async function initializeTreeSitter(): Promise<WebTreeSitter.Parser> {
|
567
|
+
try {
|
568
|
+
// Handle different environments
|
569
|
+
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node
|
570
|
+
|
571
|
+
if (isNode) {
|
572
|
+
// Node.js environment (tests)
|
573
|
+
const fs = await import('node:fs/promises')
|
574
|
+
const path = await import('node:path')
|
575
|
+
|
576
|
+
// Find the actual WASM files in node_modules
|
577
|
+
const treeSitterWasmPath = path.join(process.cwd(), 'node_modules/web-tree-sitter/tree-sitter.wasm')
|
578
|
+
const graphqlWasmPath = path.join(process.cwd(), 'node_modules/tree-sitter-graphql-grammar-wasm/grammar.wasm')
|
579
|
+
|
580
|
+
await WebTreeSitter.Parser.init({
|
581
|
+
locateFile: (filename: string) => {
|
582
|
+
if (filename === 'tree-sitter.wasm') {
|
583
|
+
return treeSitterWasmPath
|
584
|
+
}
|
585
|
+
return filename
|
586
|
+
},
|
587
|
+
})
|
588
|
+
|
589
|
+
const parser = new WebTreeSitter.Parser()
|
590
|
+
const wasmBuffer = await fs.readFile(graphqlWasmPath)
|
591
|
+
const GraphQL = await WebTreeSitter.Language.load(new Uint8Array(wasmBuffer))
|
592
|
+
parser.setLanguage(GraphQL)
|
593
|
+
|
594
|
+
return parser
|
595
|
+
} else {
|
596
|
+
// Browser/Vite environment
|
597
|
+
await WebTreeSitter.Parser.init({
|
598
|
+
locateFile: (filename: string) => {
|
599
|
+
if (filename === 'tree-sitter.wasm') {
|
600
|
+
return treeSitterWasmUrl
|
601
|
+
}
|
602
|
+
return filename
|
603
|
+
},
|
604
|
+
})
|
605
|
+
|
606
|
+
const parser = new WebTreeSitter.Parser()
|
607
|
+
|
608
|
+
// Fetch the WASM file as a buffer
|
609
|
+
const response = await fetch(graphqlWasmUrl)
|
610
|
+
if (!response.ok) {
|
611
|
+
throw new Error(
|
612
|
+
`Failed to load GraphQL grammar file: ${response.status} ${response.statusText}. `
|
613
|
+
+ `This may indicate a network issue or missing grammar file.`,
|
614
|
+
)
|
615
|
+
}
|
616
|
+
|
617
|
+
const wasmBuffer = await response.arrayBuffer()
|
618
|
+
|
619
|
+
if (wasmBuffer.byteLength === 0) {
|
620
|
+
throw new Error('GraphQL grammar file is empty or corrupted')
|
621
|
+
}
|
622
|
+
|
623
|
+
const GraphQL = await WebTreeSitter.Language.load(new Uint8Array(wasmBuffer))
|
624
|
+
parser.setLanguage(GraphQL)
|
625
|
+
|
626
|
+
return parser
|
627
|
+
}
|
628
|
+
} catch (error) {
|
629
|
+
// Enhance error messages for common issues
|
630
|
+
if (error instanceof Error) {
|
631
|
+
if (error.message.includes('fetch')) {
|
632
|
+
throw new Error(`Tree-sitter initialization failed: ${error.message}. Check your network connection.`)
|
633
|
+
}
|
634
|
+
if (error.message.includes('Language.load')) {
|
635
|
+
throw new Error(`Failed to load GraphQL grammar: ${error.message}. The grammar file may be corrupted.`)
|
636
|
+
}
|
637
|
+
}
|
638
|
+
throw error
|
639
|
+
}
|
640
|
+
}
|
641
|
+
|
642
|
+
/**
|
643
|
+
* Add error hint tokens after invalid fields
|
644
|
+
*/
|
645
|
+
function addErrorHintTokens(
|
646
|
+
tokens: GraphQLToken[],
|
647
|
+
code: string,
|
648
|
+
annotations: CodeAnnotation[],
|
649
|
+
): GraphQLToken[] {
|
650
|
+
const tokensWithHints: GraphQLToken[] = []
|
651
|
+
const processedIndices = new Set<number>()
|
652
|
+
|
653
|
+
// Count invalid fields for debugging
|
654
|
+
let invalidFieldCount = 0
|
655
|
+
tokens.forEach(t => {
|
656
|
+
if (t.semantic && 'kind' in t.semantic && t.semantic.kind === 'InvalidField') {
|
657
|
+
invalidFieldCount++
|
658
|
+
}
|
659
|
+
})
|
660
|
+
|
661
|
+
if (invalidFieldCount > 10) {
|
662
|
+
// Too many invalid fields - likely a schema mismatch
|
663
|
+
// Return tokens without error hints to avoid corrupting display
|
664
|
+
console.warn(`Polen: ${invalidFieldCount} invalid fields detected. Schema may not match queries.`)
|
665
|
+
return tokens
|
666
|
+
}
|
667
|
+
|
668
|
+
for (let i = 0; i < tokens.length; i++) {
|
669
|
+
const token = tokens[i]!
|
670
|
+
|
671
|
+
// Skip if we've already processed this token (due to lookahead for arguments)
|
672
|
+
if (processedIndices.has(i)) {
|
673
|
+
continue
|
674
|
+
}
|
675
|
+
|
676
|
+
tokensWithHints.push(token)
|
677
|
+
processedIndices.add(i)
|
678
|
+
|
679
|
+
// Check if this is an invalid field
|
680
|
+
if (token.semantic && 'kind' in token.semantic && token.semantic.kind === 'InvalidField') {
|
681
|
+
// Look ahead to find where the field ends (after arguments if present)
|
682
|
+
let fieldEndIndex = i
|
683
|
+
let j = i + 1
|
684
|
+
|
685
|
+
// Skip whitespace
|
686
|
+
while (j < tokens.length && tokens[j]!.treeSitterNode.type === 'whitespace') {
|
687
|
+
j++
|
688
|
+
}
|
689
|
+
|
690
|
+
// Check if we have arguments starting with '('
|
691
|
+
if (j < tokens.length && tokens[j]!.text === '(') {
|
692
|
+
// Find the matching closing ')'
|
693
|
+
let parenDepth = 1
|
694
|
+
j++ // move past the opening '('
|
695
|
+
|
696
|
+
while (j < tokens.length && parenDepth > 0) {
|
697
|
+
const t = tokens[j]!
|
698
|
+
if (t.text === '(') parenDepth++
|
699
|
+
else if (t.text === ')') parenDepth--
|
700
|
+
j++
|
701
|
+
}
|
702
|
+
|
703
|
+
// j is now past the closing ')'
|
704
|
+
fieldEndIndex = j - 1
|
705
|
+
|
706
|
+
// Add all tokens that are part of the field's arguments
|
707
|
+
for (let k = i + 1; k <= fieldEndIndex; k++) {
|
708
|
+
if (k < tokens.length && !processedIndices.has(k)) {
|
709
|
+
tokensWithHints.push(tokens[k]!)
|
710
|
+
processedIndices.add(k)
|
711
|
+
}
|
712
|
+
}
|
713
|
+
}
|
714
|
+
|
715
|
+
// Now add the error hint after the complete field (including arguments)
|
716
|
+
const lastFieldToken = tokens[fieldEndIndex] || token
|
717
|
+
const hintText = ' ← No such field'
|
718
|
+
const hintToken = new UnifiedToken(
|
719
|
+
createSyntheticNode(
|
720
|
+
'error_hint',
|
721
|
+
hintText,
|
722
|
+
lastFieldToken.end,
|
723
|
+
lastFieldToken.end + hintText.length,
|
724
|
+
),
|
725
|
+
undefined,
|
726
|
+
annotations,
|
727
|
+
)
|
728
|
+
tokensWithHints.push(hintToken)
|
729
|
+
}
|
730
|
+
}
|
731
|
+
|
732
|
+
return tokensWithHints
|
733
|
+
}
|
734
|
+
|
735
|
+
/**
|
736
|
+
* Walk tree-sitter AST and collect tokens with semantic information
|
737
|
+
*/
|
738
|
+
function collectTokensWithSemantics(
|
739
|
+
tree: WebTreeSitter.Tree,
|
740
|
+
code: string,
|
741
|
+
schema: GraphQLSchema | undefined,
|
742
|
+
annotations: CodeAnnotation[],
|
743
|
+
): GraphQLToken[] {
|
744
|
+
const tokens: GraphQLToken[] = []
|
745
|
+
const cursor = tree.walk()
|
746
|
+
const context = schema ? new SemanticContext(schema) : null
|
747
|
+
let lastEnd = 0
|
748
|
+
|
749
|
+
function processNode() {
|
750
|
+
const node = cursor.currentNode
|
751
|
+
if (!node) return
|
752
|
+
|
753
|
+
// Handle different node types for semantic context
|
754
|
+
if (context) {
|
755
|
+
if (node.type === 'operation_definition') {
|
756
|
+
// Find the operation type child
|
757
|
+
const operationType = findChildByType(cursor, 'operation_type')
|
758
|
+
if (operationType) {
|
759
|
+
context.enterOperation(operationType.text)
|
760
|
+
}
|
761
|
+
}
|
762
|
+
|
763
|
+
if (node.type === 'fragment_definition') {
|
764
|
+
// Find the type condition
|
765
|
+
const typeCondition = findChildByType(cursor, 'type_condition')
|
766
|
+
if (typeCondition) {
|
767
|
+
const typeName = findChildByType(cursor, 'named_type', typeCondition)
|
768
|
+
if (typeName) {
|
769
|
+
const nameNode = findChildByType(cursor, 'name', typeName)
|
770
|
+
if (nameNode) {
|
771
|
+
context.enterFragment(nameNode.text)
|
772
|
+
}
|
773
|
+
}
|
774
|
+
}
|
775
|
+
}
|
776
|
+
|
777
|
+
// We don't need special handling for selection_set anymore
|
778
|
+
// Context is managed at the field level
|
779
|
+
}
|
780
|
+
|
781
|
+
// Collect leaf tokens with semantic info
|
782
|
+
// Special case: string_value, int_value, float_value nodes should be collected as whole tokens
|
783
|
+
// even though they have children (the quotes or signs)
|
784
|
+
const isValueNode = node.type === 'string_value' || node.type === 'int_value' || node.type === 'float_value'
|
785
|
+
const shouldCollectToken = (node.childCount === 0 || isValueNode) && node.text.trim() !== ''
|
786
|
+
|
787
|
+
if (shouldCollectToken) {
|
788
|
+
// Add whitespace before this token if needed
|
789
|
+
if (node.startIndex > lastEnd) {
|
790
|
+
const whitespace = code.slice(lastEnd, node.startIndex)
|
791
|
+
tokens.push(
|
792
|
+
new UnifiedToken(
|
793
|
+
createWhitespaceNode(whitespace, lastEnd, node.startIndex),
|
794
|
+
undefined,
|
795
|
+
annotations,
|
796
|
+
),
|
797
|
+
)
|
798
|
+
}
|
799
|
+
|
800
|
+
// Determine semantic info for this token
|
801
|
+
let semantic: SemanticNode | undefined
|
802
|
+
|
803
|
+
if (context && node.type === 'name') {
|
804
|
+
const parent = cursor.currentNode.parent
|
805
|
+
|
806
|
+
if (parent?.type === 'field') {
|
807
|
+
// This is a field name - get info from current context
|
808
|
+
const fieldInfo = context.getFieldInfo(node.text)
|
809
|
+
const currentType = context.getCurrentType()
|
810
|
+
|
811
|
+
if (fieldInfo) {
|
812
|
+
semantic = {
|
813
|
+
kind: 'OutputField',
|
814
|
+
parentType: fieldInfo.parentType,
|
815
|
+
fieldDef: fieldInfo.fieldDef,
|
816
|
+
}
|
817
|
+
// Enter this field's context for processing its selection set
|
818
|
+
context.enterField(node.text)
|
819
|
+
} else if (currentType) {
|
820
|
+
// Field doesn't exist - mark as invalid
|
821
|
+
semantic = {
|
822
|
+
kind: 'InvalidField',
|
823
|
+
fieldName: node.text,
|
824
|
+
parentType: currentType,
|
825
|
+
}
|
826
|
+
}
|
827
|
+
} else if (parent?.type === 'named_type') {
|
828
|
+
// This is a type reference
|
829
|
+
const type = context.lookupType(node.text)
|
830
|
+
if (type) {
|
831
|
+
// Check if it's one of the types we support as semantic nodes
|
832
|
+
if (
|
833
|
+
type instanceof GraphQLObjectType
|
834
|
+
|| type instanceof GraphQLScalarType
|
835
|
+
|| type instanceof GraphQLInterfaceType
|
836
|
+
|| type instanceof GraphQLUnionType
|
837
|
+
|| type instanceof GraphQLEnumType
|
838
|
+
|| type instanceof GraphQLInputObjectType
|
839
|
+
) {
|
840
|
+
semantic = type
|
841
|
+
}
|
842
|
+
}
|
843
|
+
} else if (parent?.type === 'operation_definition') {
|
844
|
+
// This is an operation name
|
845
|
+
semantic = {
|
846
|
+
kind: 'Operation',
|
847
|
+
type: context.operationType || 'query',
|
848
|
+
name: node.text,
|
849
|
+
}
|
850
|
+
} else if (parent?.type === 'fragment_definition') {
|
851
|
+
// This is a fragment name - for now just mark it as a fragment
|
852
|
+
semantic = {
|
853
|
+
kind: 'Fragment',
|
854
|
+
name: node.text,
|
855
|
+
onType: context.getCurrentType()!, // We'll have the type from enterFragment
|
856
|
+
}
|
857
|
+
} else if (parent?.type === 'argument') {
|
858
|
+
// This is an argument name
|
859
|
+
//
|
860
|
+
// ## Complex Argument Parsing Logic
|
861
|
+
//
|
862
|
+
// Arguments require complex tree traversal because they appear in the tree-sitter AST
|
863
|
+
// before the semantic context has been updated for their parent field. This creates
|
864
|
+
// a chicken-and-egg problem where we need the field to identify the argument, but
|
865
|
+
// the field hasn't been processed yet.
|
866
|
+
//
|
867
|
+
// Tree structure: field > arguments > argument > name
|
868
|
+
// Processing order: argument names are parsed before field context is established
|
869
|
+
//
|
870
|
+
// Our solution is to traverse up the AST to find the field node, then look for that
|
871
|
+
// field in both the root operation type and the current type context. We check the
|
872
|
+
// root type first because top-level fields (like Query.pokemon) are most common.
|
873
|
+
|
874
|
+
let argumentsNode = parent.parent
|
875
|
+
if (argumentsNode && argumentsNode.type === 'arguments') {
|
876
|
+
let fieldNode = argumentsNode.parent
|
877
|
+
if (fieldNode && fieldNode.type === 'field') {
|
878
|
+
// Find the field name node within the field node
|
879
|
+
for (let i = 0; i < fieldNode.childCount; i++) {
|
880
|
+
const child = fieldNode.child(i)
|
881
|
+
if (child && child.type === 'name') {
|
882
|
+
// We need to find the parent type that contains this field
|
883
|
+
// Start with the root type based on the operation type (query/mutation/subscription)
|
884
|
+
const rootType = context.schema.getQueryType() || context.schema.getMutationType()
|
885
|
+
|| context.schema.getSubscriptionType()
|
886
|
+
|
887
|
+
if (rootType) {
|
888
|
+
// First check if the field exists on the root type (most common case)
|
889
|
+
let field = rootType.getFields()[child.text]
|
890
|
+
let parentType: GraphQLObjectType | GraphQLInterfaceType = rootType
|
891
|
+
|
892
|
+
// If not found on root, check the current type in our semantic context
|
893
|
+
// This handles nested field arguments like User.posts(limit: 10)
|
894
|
+
if (!field) {
|
895
|
+
const currentType = context.getCurrentType()
|
896
|
+
if (currentType) {
|
897
|
+
field = currentType.getFields()[child.text]
|
898
|
+
parentType = currentType
|
899
|
+
}
|
900
|
+
}
|
901
|
+
|
902
|
+
if (field && parentType) {
|
903
|
+
const arg = field.args.find((a: GraphQLArgument) => a.name === node.text)
|
904
|
+
if (arg) {
|
905
|
+
semantic = {
|
906
|
+
kind: 'Argument',
|
907
|
+
parentType: parentType,
|
908
|
+
parentField: field,
|
909
|
+
argumentDef: arg,
|
910
|
+
}
|
911
|
+
}
|
912
|
+
}
|
913
|
+
}
|
914
|
+
break
|
915
|
+
}
|
916
|
+
}
|
917
|
+
}
|
918
|
+
}
|
919
|
+
} else if (parent?.type === 'variable') {
|
920
|
+
// This is a variable name (without the $)
|
921
|
+
semantic = {
|
922
|
+
kind: 'Variable',
|
923
|
+
name: node.text,
|
924
|
+
}
|
925
|
+
} else if (parent?.type === 'variable_definition') {
|
926
|
+
// This is a variable definition in the operation header
|
927
|
+
semantic = {
|
928
|
+
kind: 'Variable',
|
929
|
+
name: node.text,
|
930
|
+
}
|
931
|
+
}
|
932
|
+
} else if (context && node.type === 'variable' && node.text.startsWith('$')) {
|
933
|
+
// This is the full variable including $ (usage in arguments or directives)
|
934
|
+
semantic = {
|
935
|
+
kind: 'Variable',
|
936
|
+
name: node.text.slice(1),
|
937
|
+
}
|
938
|
+
}
|
939
|
+
|
940
|
+
const token = new UnifiedToken(
|
941
|
+
node,
|
942
|
+
semantic,
|
943
|
+
annotations,
|
944
|
+
)
|
945
|
+
|
946
|
+
tokens.push(token)
|
947
|
+
|
948
|
+
lastEnd = node.endIndex
|
949
|
+
}
|
950
|
+
|
951
|
+
// Traverse children (but skip children of value nodes since we collect them as whole tokens)
|
952
|
+
if (!isValueNode && cursor.gotoFirstChild()) {
|
953
|
+
do {
|
954
|
+
processNode()
|
955
|
+
} while (cursor.gotoNextSibling())
|
956
|
+
cursor.gotoParent()
|
957
|
+
|
958
|
+
// Handle context exit
|
959
|
+
if (context && node.type === 'field') {
|
960
|
+
// Only exit field context if this field has a selection set
|
961
|
+
// (meaning it's an object/interface type that pushed to the stack)
|
962
|
+
const hasSelectionSet = node.childCount > 0 && node.children.some(
|
963
|
+
child => child?.type === 'selection_set',
|
964
|
+
)
|
965
|
+
if (hasSelectionSet) {
|
966
|
+
context.exitField()
|
967
|
+
}
|
968
|
+
} else if (context && (node.type === 'operation_definition' || node.type === 'fragment_definition')) {
|
969
|
+
// Reset context when exiting operation or fragment
|
970
|
+
context.reset()
|
971
|
+
}
|
972
|
+
}
|
973
|
+
}
|
974
|
+
|
975
|
+
processNode()
|
976
|
+
|
977
|
+
// Add final whitespace if needed
|
978
|
+
if (lastEnd < code.length) {
|
979
|
+
const remaining = code.slice(lastEnd)
|
980
|
+
tokens.push(
|
981
|
+
new UnifiedToken(
|
982
|
+
createWhitespaceNode(remaining, lastEnd, code.length),
|
983
|
+
undefined,
|
984
|
+
annotations,
|
985
|
+
),
|
986
|
+
)
|
987
|
+
}
|
988
|
+
|
989
|
+
return tokens
|
990
|
+
}
|
991
|
+
|
992
|
+
/**
|
993
|
+
* Helper to find a child node by type
|
994
|
+
*/
|
995
|
+
function findChildByType(
|
996
|
+
cursor: WebTreeSitter.TreeCursor,
|
997
|
+
type: string,
|
998
|
+
node?: WebTreeSitter.Node,
|
999
|
+
): WebTreeSitter.Node | null {
|
1000
|
+
const targetNode = node || cursor.currentNode
|
1001
|
+
if (!targetNode) return null
|
1002
|
+
|
1003
|
+
for (let i = 0; i < targetNode.childCount; i++) {
|
1004
|
+
const child = targetNode.child(i)
|
1005
|
+
if (child && child.type === type) {
|
1006
|
+
return child
|
1007
|
+
}
|
1008
|
+
}
|
1009
|
+
return null
|
1010
|
+
}
|
1011
|
+
|
1012
|
+
/**
|
1013
|
+
* Create a pseudo tree-sitter node for whitespace
|
1014
|
+
*/
|
1015
|
+
function createWhitespaceNode(text: string, start: number, end: number): WebTreeSitter.Node {
|
1016
|
+
// Create a synthetic node with proper getter interface
|
1017
|
+
const node = new SyntheticNode('whitespace', text, start, end)
|
1018
|
+
return node as unknown as WebTreeSitter.Node
|
1019
|
+
}
|
1020
|
+
|
1021
|
+
/**
|
1022
|
+
* Create a pseudo tree-sitter node for synthetic content
|
1023
|
+
*
|
1024
|
+
* ## Annotation Architecture
|
1025
|
+
*
|
1026
|
+
* Polen uses synthetic tree-sitter nodes to inject additional content into GraphQL code blocks.
|
1027
|
+
* This approach was chosen after considering several alternatives:
|
1028
|
+
*
|
1029
|
+
* ### Current Approach: Synthetic Nodes
|
1030
|
+
* We create fake tree-sitter nodes that implement just enough of the Node interface to flow
|
1031
|
+
* through our token rendering pipeline. This is used for error hints that appear after invalid fields.
|
1032
|
+
*
|
1033
|
+
* **When to use**: When you need to add new content to the code block (not just style existing content)
|
1034
|
+
*
|
1035
|
+
* ### Alternative Approaches Considered:
|
1036
|
+
*
|
1037
|
+
* 1. **CodeHike Annotations**
|
1038
|
+
* - Use CodeHike's built-in InlineAnnotation/BlockAnnotation system
|
1039
|
+
* - Pros: Works with CodeHike's architecture, composable with other handlers
|
1040
|
+
* - Cons: More complex, requires understanding CodeHike's annotation pipeline
|
1041
|
+
* - Best for: Complex features like collapsible sections, tabs
|
1042
|
+
*
|
1043
|
+
* 2. **Post-Processing During Render**
|
1044
|
+
* - Keep tokens unchanged, add content during the rendering phase
|
1045
|
+
* - Pros: Simpler, no fake nodes needed
|
1046
|
+
* - Cons: Rendering logic becomes more complex, harder to test
|
1047
|
+
* - Best for: Simple conditional content
|
1048
|
+
*
|
1049
|
+
* 3. **Token Metadata/Props**
|
1050
|
+
* - Add annotation data to token properties rather than creating new tokens
|
1051
|
+
* - Pros: Clean data model, easy to test
|
1052
|
+
* - Cons: Can't add new content, only modify existing tokens
|
1053
|
+
* - Best for: Styling annotations (highlights, emphasis, underlines)
|
1054
|
+
*
|
1055
|
+
* ### Guidelines for Future Annotations:
|
1056
|
+
*
|
1057
|
+
* - **Styling only** (highlights, emphasis): Use token metadata/props
|
1058
|
+
* - **Adding content** (error hints, tooltips): Use synthetic nodes (current approach)
|
1059
|
+
* - **Complex UI** (collapsible, tabs): Consider CodeHike annotation handlers
|
1060
|
+
* - **User-defined annotations**: Choose based on what the annotation does
|
1061
|
+
*
|
1062
|
+
* The synthetic node approach works well for Polen's error hints because we're actually
|
1063
|
+
* inserting new text content ("← No such field") that needs to be positioned and styled
|
1064
|
+
* like a regular token.
|
1065
|
+
*/
|
1066
|
+
function createSyntheticNode(
|
1067
|
+
type: TreeSitterGraphQLNodeType,
|
1068
|
+
text: string,
|
1069
|
+
start: number,
|
1070
|
+
end: number,
|
1071
|
+
): WebTreeSitter.Node {
|
1072
|
+
// Create a synthetic node with proper getter interface
|
1073
|
+
const node = new SyntheticNode(type, text, start, end)
|
1074
|
+
return node as unknown as WebTreeSitter.Node
|
1075
|
+
}
|