@sanity/groq-lsp 0.0.1
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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/chunk-UOIHOMN2.js +703 -0
- package/dist/chunk-UOIHOMN2.js.map +1 -0
- package/dist/index.d.ts +271 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +185 -0
- package/dist/server.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
// src/schema/loader.ts
|
|
2
|
+
import { readFileSync, existsSync, statSync, watch } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var SCHEMA_CANDIDATES = [
|
|
5
|
+
"schema.json",
|
|
6
|
+
"sanity.schema.json",
|
|
7
|
+
".sanity/schema.json",
|
|
8
|
+
"studio/schema.json"
|
|
9
|
+
];
|
|
10
|
+
var SchemaLoader = class {
|
|
11
|
+
state = {};
|
|
12
|
+
watcher = null;
|
|
13
|
+
onChangeCallback = null;
|
|
14
|
+
/**
|
|
15
|
+
* Load schema from a specific path
|
|
16
|
+
*/
|
|
17
|
+
loadFromPath(schemaPath) {
|
|
18
|
+
try {
|
|
19
|
+
if (!existsSync(schemaPath)) {
|
|
20
|
+
this.state = {
|
|
21
|
+
path: schemaPath,
|
|
22
|
+
error: `Schema file not found: ${schemaPath}`
|
|
23
|
+
};
|
|
24
|
+
return this.state;
|
|
25
|
+
}
|
|
26
|
+
const content = readFileSync(schemaPath, "utf-8");
|
|
27
|
+
const schema = JSON.parse(content);
|
|
28
|
+
const stats = statSync(schemaPath);
|
|
29
|
+
this.state = {
|
|
30
|
+
schema,
|
|
31
|
+
path: schemaPath,
|
|
32
|
+
lastModified: stats.mtimeMs
|
|
33
|
+
};
|
|
34
|
+
return this.state;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
this.state = {
|
|
37
|
+
path: schemaPath,
|
|
38
|
+
error: `Failed to load schema: ${e instanceof Error ? e.message : String(e)}`
|
|
39
|
+
};
|
|
40
|
+
return this.state;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Auto-discover schema file in workspace
|
|
45
|
+
*/
|
|
46
|
+
discoverSchema(workspaceRoot) {
|
|
47
|
+
for (const candidate of SCHEMA_CANDIDATES) {
|
|
48
|
+
const candidatePath = join(workspaceRoot, candidate);
|
|
49
|
+
if (existsSync(candidatePath)) {
|
|
50
|
+
return this.loadFromPath(candidatePath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.state = {
|
|
54
|
+
error: "No schema.json found. Generate with: npx sanity schema extract"
|
|
55
|
+
};
|
|
56
|
+
return this.state;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Get the current schema state
|
|
60
|
+
*/
|
|
61
|
+
getState() {
|
|
62
|
+
return this.state;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get the loaded schema (if any)
|
|
66
|
+
*/
|
|
67
|
+
getSchema() {
|
|
68
|
+
return this.state.schema;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if schema needs reloading (file changed)
|
|
72
|
+
*/
|
|
73
|
+
needsReload() {
|
|
74
|
+
if (!this.state.path || !existsSync(this.state.path)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const stats = statSync(this.state.path);
|
|
79
|
+
return stats.mtimeMs !== this.state.lastModified;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Reload schema if the file has changed
|
|
86
|
+
*/
|
|
87
|
+
reloadIfNeeded() {
|
|
88
|
+
if (this.needsReload() && this.state.path) {
|
|
89
|
+
this.loadFromPath(this.state.path);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Start watching the schema file for changes
|
|
96
|
+
*/
|
|
97
|
+
startWatching(onChange) {
|
|
98
|
+
this.onChangeCallback = onChange;
|
|
99
|
+
if (!this.state.path || !existsSync(this.state.path)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
this.watcher = watch(this.state.path, (eventType) => {
|
|
104
|
+
if (eventType === "change" && this.state.path) {
|
|
105
|
+
this.loadFromPath(this.state.path);
|
|
106
|
+
this.onChangeCallback?.(this.state.schema);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error(`Failed to watch schema file: ${e}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Stop watching the schema file
|
|
115
|
+
*/
|
|
116
|
+
stopWatching() {
|
|
117
|
+
this.watcher?.close();
|
|
118
|
+
this.watcher = null;
|
|
119
|
+
this.onChangeCallback = null;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Clear the loaded schema
|
|
123
|
+
*/
|
|
124
|
+
clear() {
|
|
125
|
+
this.stopWatching();
|
|
126
|
+
this.state = {};
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
var defaultLoader = null;
|
|
130
|
+
function getSchemaLoader() {
|
|
131
|
+
if (!defaultLoader) {
|
|
132
|
+
defaultLoader = new SchemaLoader();
|
|
133
|
+
}
|
|
134
|
+
return defaultLoader;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/utils/groq-extractor.ts
|
|
138
|
+
function extractQueries(content, languageId) {
|
|
139
|
+
switch (languageId) {
|
|
140
|
+
case "groq":
|
|
141
|
+
return extractFromGroqFile(content);
|
|
142
|
+
case "typescript":
|
|
143
|
+
case "typescriptreact":
|
|
144
|
+
case "javascript":
|
|
145
|
+
case "javascriptreact":
|
|
146
|
+
return extractFromJsTs(content);
|
|
147
|
+
default:
|
|
148
|
+
return { queries: [], errors: [] };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function extractFromGroqFile(content) {
|
|
152
|
+
const trimmed = content.trim();
|
|
153
|
+
if (!trimmed) {
|
|
154
|
+
return { queries: [], errors: [] };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
queries: [
|
|
158
|
+
{
|
|
159
|
+
query: trimmed,
|
|
160
|
+
start: content.indexOf(trimmed),
|
|
161
|
+
end: content.indexOf(trimmed) + trimmed.length,
|
|
162
|
+
line: 0,
|
|
163
|
+
column: 0
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
errors: []
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function extractFromJsTs(content) {
|
|
170
|
+
const queries = [];
|
|
171
|
+
const errors = [];
|
|
172
|
+
const groqTagRegex = /\bgroq\s*`([^`]*)`/g;
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = groqTagRegex.exec(content)) !== null) {
|
|
175
|
+
const queryContent = match[1];
|
|
176
|
+
if (queryContent === void 0) continue;
|
|
177
|
+
const fullMatchStart = match.index;
|
|
178
|
+
const queryStart = fullMatchStart + match[0].indexOf("`") + 1;
|
|
179
|
+
const beforeMatch = content.slice(0, queryStart);
|
|
180
|
+
const lines = beforeMatch.split("\n");
|
|
181
|
+
const line = lines.length - 1;
|
|
182
|
+
const column = lines[lines.length - 1]?.length ?? 0;
|
|
183
|
+
queries.push({
|
|
184
|
+
query: queryContent,
|
|
185
|
+
start: queryStart,
|
|
186
|
+
end: queryStart + queryContent.length,
|
|
187
|
+
line,
|
|
188
|
+
column
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return { queries, errors };
|
|
192
|
+
}
|
|
193
|
+
function findQueryAtOffset(queries, offset) {
|
|
194
|
+
return queries.find((q) => offset >= q.start && offset <= q.end);
|
|
195
|
+
}
|
|
196
|
+
function offsetToQueryPosition(query, documentOffset) {
|
|
197
|
+
return documentOffset - query.start;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/capabilities/diagnostics.ts
|
|
201
|
+
import { lint } from "@sanity/groq-lint";
|
|
202
|
+
function computeQueryDiagnostics(query, options = {}) {
|
|
203
|
+
const result = lint(query.query, options.schema ? { schema: options.schema } : void 0);
|
|
204
|
+
if (result.parseError) {
|
|
205
|
+
const diagnostic = {
|
|
206
|
+
range: {
|
|
207
|
+
start: { line: query.line, character: query.column },
|
|
208
|
+
end: { line: query.line, character: query.column + query.query.length }
|
|
209
|
+
},
|
|
210
|
+
severity: 1,
|
|
211
|
+
// Error
|
|
212
|
+
source: "groq",
|
|
213
|
+
message: `Parse error: ${result.parseError}`
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
query,
|
|
217
|
+
diagnostics: [diagnostic],
|
|
218
|
+
parseError: result.parseError
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const diagnostics = result.findings.map((finding) => findingToDiagnostic(finding, query));
|
|
222
|
+
return { query, diagnostics };
|
|
223
|
+
}
|
|
224
|
+
function computeDocumentDiagnostics(queries, options = {}) {
|
|
225
|
+
const allDiagnostics = [];
|
|
226
|
+
for (const query of queries) {
|
|
227
|
+
const result = computeQueryDiagnostics(query, options);
|
|
228
|
+
allDiagnostics.push(...result.diagnostics);
|
|
229
|
+
}
|
|
230
|
+
return allDiagnostics;
|
|
231
|
+
}
|
|
232
|
+
function findingToDiagnostic(finding, query) {
|
|
233
|
+
const range = findingToRange(finding, query);
|
|
234
|
+
const diagnostic = {
|
|
235
|
+
range,
|
|
236
|
+
severity: severityToLsp(finding.severity),
|
|
237
|
+
source: "groq",
|
|
238
|
+
code: finding.ruleId,
|
|
239
|
+
message: finding.message
|
|
240
|
+
};
|
|
241
|
+
if (finding.help) {
|
|
242
|
+
diagnostic.message += `
|
|
243
|
+
|
|
244
|
+
Help: ${finding.help}`;
|
|
245
|
+
}
|
|
246
|
+
return diagnostic;
|
|
247
|
+
}
|
|
248
|
+
function findingToRange(finding, query) {
|
|
249
|
+
if (finding.span) {
|
|
250
|
+
const startLine = query.line + finding.span.start.line - 1;
|
|
251
|
+
const endLine = query.line + finding.span.end.line - 1;
|
|
252
|
+
const startChar = finding.span.start.line === 1 ? query.column + finding.span.start.column - 1 : finding.span.start.column - 1;
|
|
253
|
+
const endChar = finding.span.end.line === 1 ? query.column + finding.span.end.column - 1 : finding.span.end.column - 1;
|
|
254
|
+
return {
|
|
255
|
+
start: { line: startLine, character: startChar },
|
|
256
|
+
end: { line: endLine, character: endChar }
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
start: { line: query.line, character: query.column },
|
|
261
|
+
end: { line: query.line, character: query.column + query.query.length }
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function severityToLsp(severity) {
|
|
265
|
+
switch (severity) {
|
|
266
|
+
case "error":
|
|
267
|
+
return 1;
|
|
268
|
+
// DiagnosticSeverity.Error
|
|
269
|
+
case "warning":
|
|
270
|
+
return 2;
|
|
271
|
+
// DiagnosticSeverity.Warning
|
|
272
|
+
case "info":
|
|
273
|
+
return 3;
|
|
274
|
+
// DiagnosticSeverity.Information
|
|
275
|
+
default:
|
|
276
|
+
return 4;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function positionToQueryOffset(position, query, _documentLines) {
|
|
280
|
+
const queryLines = query.query.split("\n");
|
|
281
|
+
const queryEndLine = query.line + queryLines.length - 1;
|
|
282
|
+
if (position.line < query.line || position.line > queryEndLine) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
let offset = 0;
|
|
286
|
+
for (let i = query.line; i < position.line; i++) {
|
|
287
|
+
const lineInQuery2 = i - query.line;
|
|
288
|
+
if (lineInQuery2 < queryLines.length) {
|
|
289
|
+
offset += (queryLines[lineInQuery2]?.length ?? 0) + 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const lineInQuery = position.line - query.line;
|
|
293
|
+
if (lineInQuery === 0) {
|
|
294
|
+
offset += position.character - query.column;
|
|
295
|
+
} else {
|
|
296
|
+
offset += position.character;
|
|
297
|
+
}
|
|
298
|
+
if (offset < 0 || offset > query.query.length) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
return offset;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/capabilities/hover.ts
|
|
305
|
+
import { parse, typeEvaluate } from "groq-js";
|
|
306
|
+
function getHoverInfo(query, positionInQuery, options = {}) {
|
|
307
|
+
try {
|
|
308
|
+
const ast = parse(query.query);
|
|
309
|
+
const nodeInfo = findNodeAtPosition(ast, positionInQuery, query.query);
|
|
310
|
+
if (!nodeInfo) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
let typeInfo = null;
|
|
314
|
+
if (options.schema) {
|
|
315
|
+
try {
|
|
316
|
+
const resultType = typeEvaluate(ast, options.schema);
|
|
317
|
+
typeInfo = typeNodeToInfo(resultType, nodeInfo.text);
|
|
318
|
+
} catch {
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const content = buildHoverContent(nodeInfo, typeInfo);
|
|
322
|
+
if (!content) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
contents: content
|
|
327
|
+
};
|
|
328
|
+
} catch {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function findNodeAtPosition(_ast, position, queryText) {
|
|
333
|
+
const { word, context } = extractWordAtPosition(queryText, position);
|
|
334
|
+
if (!word) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (context === "filter" && word.startsWith("_type")) {
|
|
338
|
+
return {
|
|
339
|
+
type: "filter",
|
|
340
|
+
text: word,
|
|
341
|
+
documentation: "Filters documents by their type"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (word.startsWith("_")) {
|
|
345
|
+
return {
|
|
346
|
+
type: "system-field",
|
|
347
|
+
text: word,
|
|
348
|
+
documentation: getSystemFieldDoc(word)
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
if (word === "->") {
|
|
352
|
+
return {
|
|
353
|
+
type: "dereference",
|
|
354
|
+
text: "->",
|
|
355
|
+
documentation: "Dereferences a reference to fetch the referenced document"
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const funcDoc = getGroqFunctionDoc(word);
|
|
359
|
+
if (funcDoc) {
|
|
360
|
+
return {
|
|
361
|
+
type: "function",
|
|
362
|
+
text: word,
|
|
363
|
+
documentation: funcDoc
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
type: "field",
|
|
368
|
+
text: word
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function extractWordAtPosition(text, position) {
|
|
372
|
+
let start = position;
|
|
373
|
+
let end = position;
|
|
374
|
+
while (start > 0 && isWordChar(text[start - 1])) {
|
|
375
|
+
start--;
|
|
376
|
+
}
|
|
377
|
+
while (end < text.length && isWordChar(text[end])) {
|
|
378
|
+
end++;
|
|
379
|
+
}
|
|
380
|
+
const word = text.slice(start, end);
|
|
381
|
+
const before = text.slice(0, start).trim();
|
|
382
|
+
let context = "unknown";
|
|
383
|
+
if (before.endsWith("[")) {
|
|
384
|
+
context = "filter";
|
|
385
|
+
} else if (before.endsWith("{") || before.endsWith(",")) {
|
|
386
|
+
context = "projection";
|
|
387
|
+
} else if (before.endsWith("->")) {
|
|
388
|
+
context = "dereference";
|
|
389
|
+
}
|
|
390
|
+
return { word: word || null, context };
|
|
391
|
+
}
|
|
392
|
+
function isWordChar(char) {
|
|
393
|
+
if (!char) return false;
|
|
394
|
+
return /[a-zA-Z0-9_]/.test(char);
|
|
395
|
+
}
|
|
396
|
+
function getSystemFieldDoc(field) {
|
|
397
|
+
const docs = {
|
|
398
|
+
_id: "Unique document identifier",
|
|
399
|
+
_type: "Document type name",
|
|
400
|
+
_rev: "Document revision (changes on every update)",
|
|
401
|
+
_createdAt: "Timestamp when the document was created",
|
|
402
|
+
_updatedAt: "Timestamp when the document was last updated",
|
|
403
|
+
_key: "Array item key (unique within the array)",
|
|
404
|
+
_ref: "Reference target document ID",
|
|
405
|
+
_weak: "Whether this is a weak reference"
|
|
406
|
+
};
|
|
407
|
+
return docs[field] ?? `System field: ${field}`;
|
|
408
|
+
}
|
|
409
|
+
function getGroqFunctionDoc(name) {
|
|
410
|
+
const docs = {
|
|
411
|
+
count: "count(array) - Returns the number of items in an array",
|
|
412
|
+
defined: "defined(value) - Returns true if the value is not null",
|
|
413
|
+
coalesce: "coalesce(a, b, ...) - Returns the first non-null value",
|
|
414
|
+
select: "select(conditions) - Returns value based on conditions",
|
|
415
|
+
length: "length(string|array) - Returns the length",
|
|
416
|
+
lower: "lower(string) - Converts string to lowercase",
|
|
417
|
+
upper: "upper(string) - Converts string to uppercase",
|
|
418
|
+
now: "now() - Returns the current timestamp",
|
|
419
|
+
round: "round(number, precision?) - Rounds a number",
|
|
420
|
+
string: "string(value) - Converts value to string",
|
|
421
|
+
references: "references(id) - Returns true if document references the ID",
|
|
422
|
+
dateTime: "dateTime(string) - Parses an ISO 8601 date string",
|
|
423
|
+
boost: "boost(condition, value) - Boosts score in text search",
|
|
424
|
+
score: "score(conditions) - Returns relevance score",
|
|
425
|
+
order: "order(field, direction?) - Sorts results",
|
|
426
|
+
pt: "pt::text(blocks) - Extracts plain text from Portable Text",
|
|
427
|
+
geo: "geo::distance(a, b) - Calculates distance between points",
|
|
428
|
+
math: "math::sum(array), math::avg(array), etc. - Math operations",
|
|
429
|
+
array: "array::unique(arr), array::compact(arr) - Array operations",
|
|
430
|
+
sanity: "sanity::projectId(), sanity::dataset() - Sanity project info"
|
|
431
|
+
};
|
|
432
|
+
return docs[name] ?? null;
|
|
433
|
+
}
|
|
434
|
+
function typeNodeToInfo(typeNode, fieldName) {
|
|
435
|
+
const typeStr = formatTypeNode(typeNode);
|
|
436
|
+
return {
|
|
437
|
+
type: typeStr,
|
|
438
|
+
schemaType: fieldName
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function formatTypeNode(node) {
|
|
442
|
+
switch (node.type) {
|
|
443
|
+
case "null":
|
|
444
|
+
return "null";
|
|
445
|
+
case "boolean":
|
|
446
|
+
return "boolean";
|
|
447
|
+
case "number":
|
|
448
|
+
return "number";
|
|
449
|
+
case "string":
|
|
450
|
+
return node.value !== void 0 ? `"${node.value}"` : "string";
|
|
451
|
+
case "array":
|
|
452
|
+
return `array<${formatTypeNode(node.of)}>`;
|
|
453
|
+
case "object":
|
|
454
|
+
return "object";
|
|
455
|
+
case "union":
|
|
456
|
+
if (node.of.length <= 3) {
|
|
457
|
+
return node.of.map(formatTypeNode).join(" | ");
|
|
458
|
+
}
|
|
459
|
+
return `union<${node.of.length} types>`;
|
|
460
|
+
case "unknown":
|
|
461
|
+
return "unknown";
|
|
462
|
+
case "inline":
|
|
463
|
+
return node.name;
|
|
464
|
+
default:
|
|
465
|
+
return "unknown";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function buildHoverContent(nodeInfo, typeInfo) {
|
|
469
|
+
const parts = [];
|
|
470
|
+
if (typeInfo) {
|
|
471
|
+
parts.push("```groq");
|
|
472
|
+
parts.push(`${nodeInfo.text}: ${typeInfo.type}`);
|
|
473
|
+
parts.push("```");
|
|
474
|
+
} else {
|
|
475
|
+
parts.push(`**${nodeInfo.text}** (${nodeInfo.type})`);
|
|
476
|
+
}
|
|
477
|
+
if (nodeInfo.documentation) {
|
|
478
|
+
parts.push("");
|
|
479
|
+
parts.push(nodeInfo.documentation);
|
|
480
|
+
}
|
|
481
|
+
if (parts.length === 0) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
kind: "markdown",
|
|
486
|
+
value: parts.join("\n")
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/capabilities/completion.ts
|
|
491
|
+
function getCompletions(query, positionInQuery, options = {}) {
|
|
492
|
+
const context = analyzeCompletionContext(query.query, positionInQuery);
|
|
493
|
+
switch (context.kind) {
|
|
494
|
+
case "type-value":
|
|
495
|
+
return getTypeValueCompletions(options.schema, context.prefix);
|
|
496
|
+
case "field":
|
|
497
|
+
return getFieldCompletions(options.schema, context.documentType, context.prefix);
|
|
498
|
+
case "function":
|
|
499
|
+
return getFunctionCompletions(context.prefix);
|
|
500
|
+
default:
|
|
501
|
+
return [
|
|
502
|
+
...getFieldCompletions(options.schema, context.documentType, context.prefix),
|
|
503
|
+
...getFunctionCompletions(context.prefix),
|
|
504
|
+
...getSystemFieldCompletions(context.prefix)
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function analyzeCompletionContext(query, position) {
|
|
509
|
+
const before = query.slice(0, position);
|
|
510
|
+
const prefix = extractPrefix(before);
|
|
511
|
+
if (/_type\s*[=!]=\s*["']$/.test(before) || /_type\s*[=!]=\s*["'][a-zA-Z0-9]*$/.test(before)) {
|
|
512
|
+
return { kind: "type-value", prefix: prefix.replace(/["']/g, "") };
|
|
513
|
+
}
|
|
514
|
+
const typeMatch = before.match(/_type\s*==\s*["'](\w+)["']/i);
|
|
515
|
+
const documentType = typeMatch?.[1];
|
|
516
|
+
if (/\.\s*\w*$/.test(before) || /{\s*[\w,\s]*$/.test(before)) {
|
|
517
|
+
return { kind: "field", documentType, prefix };
|
|
518
|
+
}
|
|
519
|
+
if (/[a-z]+$/.test(prefix) && !/->/.test(before.slice(-10))) {
|
|
520
|
+
return { kind: "function", prefix };
|
|
521
|
+
}
|
|
522
|
+
return { kind: "unknown", documentType, prefix };
|
|
523
|
+
}
|
|
524
|
+
function extractPrefix(text) {
|
|
525
|
+
const match = text.match(/[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
526
|
+
return match?.[0] ?? "";
|
|
527
|
+
}
|
|
528
|
+
function getTypeValueCompletions(schema, prefix) {
|
|
529
|
+
if (!schema || !Array.isArray(schema)) {
|
|
530
|
+
return [];
|
|
531
|
+
}
|
|
532
|
+
return schema.filter(
|
|
533
|
+
(type) => type.type === "document" && type.name.toLowerCase().includes(prefix.toLowerCase())
|
|
534
|
+
).map((type) => ({
|
|
535
|
+
label: type.name,
|
|
536
|
+
kind: 12,
|
|
537
|
+
// Value
|
|
538
|
+
detail: "Document type",
|
|
539
|
+
insertText: type.name,
|
|
540
|
+
documentation: `Document type: ${type.name}`
|
|
541
|
+
}));
|
|
542
|
+
}
|
|
543
|
+
function getFieldCompletions(schema, documentType, prefix) {
|
|
544
|
+
const completions = [];
|
|
545
|
+
completions.push(...getSystemFieldCompletions(prefix));
|
|
546
|
+
if (schema && Array.isArray(schema) && documentType) {
|
|
547
|
+
const typeSchema = schema.find((t) => t.name === documentType);
|
|
548
|
+
if (typeSchema && "attributes" in typeSchema && typeSchema.attributes) {
|
|
549
|
+
for (const [fieldName, _fieldDef] of Object.entries(typeSchema.attributes)) {
|
|
550
|
+
if (fieldName.toLowerCase().includes(prefix.toLowerCase())) {
|
|
551
|
+
completions.push({
|
|
552
|
+
label: fieldName,
|
|
553
|
+
kind: 5,
|
|
554
|
+
// Field
|
|
555
|
+
detail: `Field on ${documentType}`,
|
|
556
|
+
insertText: fieldName
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return completions;
|
|
563
|
+
}
|
|
564
|
+
function getSystemFieldCompletions(prefix) {
|
|
565
|
+
const systemFields = [
|
|
566
|
+
{ name: "_id", doc: "Unique document identifier" },
|
|
567
|
+
{ name: "_type", doc: "Document type name" },
|
|
568
|
+
{ name: "_rev", doc: "Document revision" },
|
|
569
|
+
{ name: "_createdAt", doc: "Creation timestamp" },
|
|
570
|
+
{ name: "_updatedAt", doc: "Last update timestamp" },
|
|
571
|
+
{ name: "_key", doc: "Array item key" },
|
|
572
|
+
{ name: "_ref", doc: "Reference target ID" }
|
|
573
|
+
];
|
|
574
|
+
return systemFields.filter((f) => f.name.toLowerCase().includes(prefix.toLowerCase())).map((f) => ({
|
|
575
|
+
label: f.name,
|
|
576
|
+
kind: 5,
|
|
577
|
+
// Field
|
|
578
|
+
detail: "System field",
|
|
579
|
+
documentation: f.doc,
|
|
580
|
+
insertText: f.name
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
function getFunctionCompletions(prefix) {
|
|
584
|
+
const functions = [
|
|
585
|
+
{ name: "count", sig: "count(array)", doc: "Returns the number of items" },
|
|
586
|
+
{ name: "defined", sig: "defined(value)", doc: "Returns true if value is not null" },
|
|
587
|
+
{ name: "coalesce", sig: "coalesce(a, b, ...)", doc: "Returns first non-null value" },
|
|
588
|
+
{ name: "select", sig: "select(cond => val, ...)", doc: "Conditional value selection" },
|
|
589
|
+
{ name: "length", sig: "length(str|arr)", doc: "Returns length" },
|
|
590
|
+
{ name: "lower", sig: "lower(string)", doc: "Converts to lowercase" },
|
|
591
|
+
{ name: "upper", sig: "upper(string)", doc: "Converts to uppercase" },
|
|
592
|
+
{ name: "now", sig: "now()", doc: "Current timestamp" },
|
|
593
|
+
{ name: "round", sig: "round(num, precision?)", doc: "Rounds a number" },
|
|
594
|
+
{ name: "string", sig: "string(value)", doc: "Converts to string" },
|
|
595
|
+
{ name: "references", sig: "references(id)", doc: "Checks if document references ID" },
|
|
596
|
+
{ name: "dateTime", sig: "dateTime(string)", doc: "Parses ISO 8601 date" },
|
|
597
|
+
{ name: "order", sig: "order(field, dir?)", doc: "Sorts results" },
|
|
598
|
+
{ name: "score", sig: "score(...conditions)", doc: "Relevance scoring" },
|
|
599
|
+
{ name: "boost", sig: "boost(cond, value)", doc: "Boosts search score" },
|
|
600
|
+
// Namespace functions
|
|
601
|
+
{ name: "pt::text", sig: "pt::text(blocks)", doc: "Extract text from Portable Text" },
|
|
602
|
+
{ name: "geo::distance", sig: "geo::distance(a, b)", doc: "Distance between points" },
|
|
603
|
+
{ name: "math::sum", sig: "math::sum(array)", doc: "Sum of numbers" },
|
|
604
|
+
{ name: "math::avg", sig: "math::avg(array)", doc: "Average of numbers" },
|
|
605
|
+
{ name: "math::min", sig: "math::min(array)", doc: "Minimum value" },
|
|
606
|
+
{ name: "math::max", sig: "math::max(array)", doc: "Maximum value" },
|
|
607
|
+
{ name: "array::unique", sig: "array::unique(arr)", doc: "Remove duplicates" },
|
|
608
|
+
{ name: "array::compact", sig: "array::compact(arr)", doc: "Remove nulls" },
|
|
609
|
+
{ name: "array::join", sig: "array::join(arr, sep)", doc: "Join into string" },
|
|
610
|
+
{ name: "string::split", sig: "string::split(str, sep)", doc: "Split string" },
|
|
611
|
+
{ name: "string::startsWith", sig: "string::startsWith(str, prefix)", doc: "Check prefix" },
|
|
612
|
+
{ name: "sanity::projectId", sig: "sanity::projectId()", doc: "Current project ID" },
|
|
613
|
+
{ name: "sanity::dataset", sig: "sanity::dataset()", doc: "Current dataset" }
|
|
614
|
+
];
|
|
615
|
+
return functions.filter((f) => f.name.toLowerCase().includes(prefix.toLowerCase())).map((f) => ({
|
|
616
|
+
label: f.name,
|
|
617
|
+
kind: 3,
|
|
618
|
+
// Function
|
|
619
|
+
detail: f.sig,
|
|
620
|
+
documentation: f.doc,
|
|
621
|
+
insertText: f.name.includes("::") ? f.name : `${f.name}($1)`,
|
|
622
|
+
insertTextFormat: 2
|
|
623
|
+
// Snippet
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
function getCompletionTriggerCharacters() {
|
|
627
|
+
return [".", '"', "'", "[", "{", ":"];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/capabilities/formatting.ts
|
|
631
|
+
async function formatQuery(query, options = {}) {
|
|
632
|
+
try {
|
|
633
|
+
const prettier = await import("prettier");
|
|
634
|
+
const groqPlugin = await import("prettier-plugin-groq");
|
|
635
|
+
const formatted = await prettier.format(query.query, {
|
|
636
|
+
parser: "groq",
|
|
637
|
+
plugins: [groqPlugin.default ?? groqPlugin],
|
|
638
|
+
tabWidth: options.tabSize ?? 2,
|
|
639
|
+
useTabs: !(options.insertSpaces ?? true),
|
|
640
|
+
printWidth: options.printWidth ?? 80
|
|
641
|
+
});
|
|
642
|
+
const trimmedFormatted = formatted.trimEnd();
|
|
643
|
+
if (trimmedFormatted === query.query) {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
const range = queryToRange(query);
|
|
647
|
+
return [
|
|
648
|
+
{
|
|
649
|
+
range,
|
|
650
|
+
newText: trimmedFormatted
|
|
651
|
+
}
|
|
652
|
+
];
|
|
653
|
+
} catch (error) {
|
|
654
|
+
console.error("Formatting failed:", error);
|
|
655
|
+
return [];
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function formatDocument(queries, _documentContent, options = {}) {
|
|
659
|
+
const edits = [];
|
|
660
|
+
const sortedQueries = [...queries].sort((a, b) => b.start - a.start);
|
|
661
|
+
for (const query of sortedQueries) {
|
|
662
|
+
const queryEdits = await formatQuery(query, options);
|
|
663
|
+
edits.push(...queryEdits);
|
|
664
|
+
}
|
|
665
|
+
return edits;
|
|
666
|
+
}
|
|
667
|
+
function queryToRange(query) {
|
|
668
|
+
const lines = query.query.split("\n");
|
|
669
|
+
const endLine = query.line + lines.length - 1;
|
|
670
|
+
const endChar = lines.length === 1 ? query.column + query.query.length : lines[lines.length - 1]?.length ?? 0;
|
|
671
|
+
return {
|
|
672
|
+
start: { line: query.line, character: query.column },
|
|
673
|
+
end: { line: endLine, character: endChar }
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
async function formatGroqFile(content, options = {}) {
|
|
677
|
+
const query = {
|
|
678
|
+
query: content.trim(),
|
|
679
|
+
start: 0,
|
|
680
|
+
end: content.length,
|
|
681
|
+
line: 0,
|
|
682
|
+
column: 0
|
|
683
|
+
};
|
|
684
|
+
return formatQuery(query, options);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export {
|
|
688
|
+
SchemaLoader,
|
|
689
|
+
getSchemaLoader,
|
|
690
|
+
extractQueries,
|
|
691
|
+
findQueryAtOffset,
|
|
692
|
+
offsetToQueryPosition,
|
|
693
|
+
computeQueryDiagnostics,
|
|
694
|
+
computeDocumentDiagnostics,
|
|
695
|
+
positionToQueryOffset,
|
|
696
|
+
getHoverInfo,
|
|
697
|
+
getCompletions,
|
|
698
|
+
getCompletionTriggerCharacters,
|
|
699
|
+
formatQuery,
|
|
700
|
+
formatDocument,
|
|
701
|
+
formatGroqFile
|
|
702
|
+
};
|
|
703
|
+
//# sourceMappingURL=chunk-UOIHOMN2.js.map
|