@syncular/typegen 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +359 -185
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +115 -63
- package/dist/query.js +60 -11
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
package/dist/lsp.js
CHANGED
|
@@ -1,56 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `syncular lsp` — a minimal `.syql` language server (DESIGN-queries.md
|
|
3
|
-
* §10, staged Q5). Zero dependencies: hand-rolled JSON-RPC over stdio with
|
|
4
|
-
* Content-Length framing, and the SAME parser/checker the generator runs —
|
|
5
|
-
* diagnostics are generate-time truth, not a re-implementation.
|
|
6
|
-
*
|
|
7
|
-
* Capabilities:
|
|
8
|
-
* - diagnostics (on open/change): parse errors, lowering errors (B1,
|
|
9
|
-
* knob conflicts), and — when a `syncular.json` is found above the file —
|
|
10
|
-
* the full SQLite-checked analysis (types, unknown columns, naming
|
|
11
|
-
* collisions).
|
|
12
|
-
* - hover: a query name shows its lowered, checked SQL (what actually
|
|
13
|
-
* runs); an `@fragment` ref shows the fragment body.
|
|
14
|
-
* - definition: an `@fragment` ref jumps to its declaration (same file —
|
|
15
|
-
* fragments are file-scoped).
|
|
16
|
-
*
|
|
17
|
-
* The server core is a pure(ish) class (`SyqlLanguageServer.handle`)
|
|
18
|
-
* so tests drive it without stdio; `runLspStdio` is the thin transport.
|
|
19
|
-
*/
|
|
1
|
+
/** Revision-1 SYQL language server backed by the compiler frontend (§21). */
|
|
20
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
|
-
import { dirname, relative, resolve } from 'node:path';
|
|
3
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
22
5
|
import { TypegenError } from './errors.js';
|
|
23
|
-
import {
|
|
6
|
+
import { formatSyql } from './fmt.js';
|
|
7
|
+
import { buildIr, loadMigrations, loadQueries, makeQueryDb } from './generate.js';
|
|
24
8
|
import { MANIFEST_FILENAME, parseManifest } from './manifest.js';
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
import { SyqlFrontendError } from './syql-lexer.js';
|
|
10
|
+
import { lowerSyqlQuery } from './syql-lowering.js';
|
|
11
|
+
import { buildSyqlModuleGraph } from './syql-modules.js';
|
|
12
|
+
import { parseSyqlSyntaxFile, } from './syql-parser.js';
|
|
13
|
+
import { analyzeSyqlSemantics, } from './syql-semantics.js';
|
|
14
|
+
import { validateSyqlProgram } from './syql-validator.js';
|
|
29
15
|
function offsetToPosition(text, offset) {
|
|
30
16
|
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
31
17
|
let line = 0;
|
|
32
18
|
let lineStart = 0;
|
|
33
|
-
for (let
|
|
34
|
-
if (text[
|
|
19
|
+
for (let index = 0; index < clamped; index += 1) {
|
|
20
|
+
if (text[index] === '\n') {
|
|
35
21
|
line += 1;
|
|
36
|
-
lineStart =
|
|
22
|
+
lineStart = index + 1;
|
|
37
23
|
}
|
|
38
24
|
}
|
|
39
25
|
return { line, character: clamped - lineStart };
|
|
40
26
|
}
|
|
41
27
|
function positionToOffset(text, position) {
|
|
42
28
|
let line = 0;
|
|
43
|
-
let
|
|
44
|
-
while (
|
|
45
|
-
if (text[
|
|
29
|
+
let index = 0;
|
|
30
|
+
while (index < text.length && line < position.line) {
|
|
31
|
+
if (text[index] === '\n')
|
|
46
32
|
line += 1;
|
|
47
|
-
|
|
33
|
+
index += 1;
|
|
48
34
|
}
|
|
49
|
-
return Math.min(text.length,
|
|
35
|
+
return Math.min(text.length, index + position.character);
|
|
50
36
|
}
|
|
51
|
-
/** The `@word` / `word` under the cursor, with its range. */
|
|
52
37
|
function wordAt(text, offset) {
|
|
53
|
-
const isWord = (
|
|
38
|
+
const isWord = (character) => character !== undefined && /[A-Za-z0-9_@]/.test(character);
|
|
54
39
|
if (!isWord(text[offset]) && !isWord(text[offset - 1]))
|
|
55
40
|
return null;
|
|
56
41
|
let start = isWord(text[offset]) ? offset : offset - 1;
|
|
@@ -65,24 +50,50 @@ function uriToPath(uri) {
|
|
|
65
50
|
return uri.startsWith('file://') ? decodeURIComponent(uri.slice(7)) : uri;
|
|
66
51
|
}
|
|
67
52
|
function findManifestDir(fromPath) {
|
|
68
|
-
let
|
|
69
|
-
for (let
|
|
70
|
-
if (existsSync(resolve(
|
|
71
|
-
return
|
|
72
|
-
const parent = dirname(
|
|
73
|
-
if (parent ===
|
|
53
|
+
let directory = dirname(fromPath);
|
|
54
|
+
for (let count = 0; count < 20; count += 1) {
|
|
55
|
+
if (existsSync(resolve(directory, MANIFEST_FILENAME)))
|
|
56
|
+
return directory;
|
|
57
|
+
const parent = dirname(directory);
|
|
58
|
+
if (parent === directory)
|
|
74
59
|
return null;
|
|
75
|
-
|
|
60
|
+
directory = parent;
|
|
76
61
|
}
|
|
77
62
|
return null;
|
|
78
63
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
function isWithin(root, file) {
|
|
65
|
+
const path = relative(root, file);
|
|
66
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..');
|
|
67
|
+
}
|
|
68
|
+
function spanRange(text, span) {
|
|
69
|
+
const start = offsetToPosition(text, span.start.offset);
|
|
70
|
+
const end = offsetToPosition(text, span.end.offset);
|
|
71
|
+
return {
|
|
72
|
+
start,
|
|
73
|
+
end: end.line === start.line && end.character === start.character
|
|
74
|
+
? { line: end.line, character: end.character + 1 }
|
|
75
|
+
: end,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function typeText(type) {
|
|
79
|
+
return type === undefined
|
|
80
|
+
? 'inferred'
|
|
81
|
+
: `${type.base}${type.nullable ? ' | null' : ''}`;
|
|
82
|
+
}
|
|
83
|
+
function parameterHover(parameter) {
|
|
84
|
+
if (parameter.kind === 'switch') {
|
|
85
|
+
return `switch \`${parameter.name}\` — false/absent or true`;
|
|
86
|
+
}
|
|
87
|
+
if (parameter.kind === 'group') {
|
|
88
|
+
return `optional group \`${parameter.name}\`\n\n${parameter.members
|
|
89
|
+
.map((member) => `- \`${member.name}: ${typeText(member.type)}\``)
|
|
90
|
+
.join('\n')}`;
|
|
91
|
+
}
|
|
92
|
+
return `${parameter.optional ? 'optional' : 'required'} input \`${parameter.name}: ${typeText(parameter.type)}\``;
|
|
93
|
+
}
|
|
82
94
|
export class SyqlLanguageServer {
|
|
83
95
|
#documents = new Map();
|
|
84
96
|
#contexts = new Map();
|
|
85
|
-
/** Handle one incoming message; returns the outgoing messages. */
|
|
86
97
|
handle(message) {
|
|
87
98
|
const { method, id } = message;
|
|
88
99
|
if (method === 'initialize') {
|
|
@@ -92,11 +103,14 @@ export class SyqlLanguageServer {
|
|
|
92
103
|
id,
|
|
93
104
|
result: {
|
|
94
105
|
capabilities: {
|
|
95
|
-
textDocumentSync: 1,
|
|
106
|
+
textDocumentSync: 1,
|
|
96
107
|
hoverProvider: true,
|
|
97
108
|
definitionProvider: true,
|
|
109
|
+
referencesProvider: true,
|
|
110
|
+
documentSymbolProvider: true,
|
|
111
|
+
documentFormattingProvider: true,
|
|
98
112
|
},
|
|
99
|
-
serverInfo: { name: 'syncular-syql-lsp', version: '0.0
|
|
113
|
+
serverInfo: { name: 'syncular-syql-lsp', version: '1.0.0' },
|
|
100
114
|
},
|
|
101
115
|
},
|
|
102
116
|
];
|
|
@@ -108,10 +122,9 @@ export class SyqlLanguageServer {
|
|
|
108
122
|
}
|
|
109
123
|
if (method === 'textDocument/didChange') {
|
|
110
124
|
const params = message.params;
|
|
111
|
-
const last = params.contentChanges
|
|
112
|
-
if (last !== undefined)
|
|
125
|
+
const last = params.contentChanges.at(-1);
|
|
126
|
+
if (last !== undefined)
|
|
113
127
|
this.#documents.set(params.textDocument.uri, last.text);
|
|
114
|
-
}
|
|
115
128
|
return [this.#publishDiagnostics(params.textDocument.uri)];
|
|
116
129
|
}
|
|
117
130
|
if (method === 'textDocument/didClose') {
|
|
@@ -119,15 +132,27 @@ export class SyqlLanguageServer {
|
|
|
119
132
|
this.#documents.delete(params.textDocument.uri);
|
|
120
133
|
return [];
|
|
121
134
|
}
|
|
135
|
+
if (method === 'workspace/didChangeWatchedFiles') {
|
|
136
|
+
this.invalidateProjects();
|
|
137
|
+
return [...this.#documents.keys()].map((uri) => this.#publishDiagnostics(uri));
|
|
138
|
+
}
|
|
122
139
|
if (method === 'textDocument/hover') {
|
|
123
140
|
return [{ jsonrpc: '2.0', id, result: this.#hover(message.params) }];
|
|
124
141
|
}
|
|
125
142
|
if (method === 'textDocument/definition') {
|
|
126
143
|
return [{ jsonrpc: '2.0', id, result: this.#definition(message.params) }];
|
|
127
144
|
}
|
|
128
|
-
if (method === '
|
|
129
|
-
return [{ jsonrpc: '2.0', id, result:
|
|
145
|
+
if (method === 'textDocument/references') {
|
|
146
|
+
return [{ jsonrpc: '2.0', id, result: this.#references(message.params) }];
|
|
147
|
+
}
|
|
148
|
+
if (method === 'textDocument/documentSymbol') {
|
|
149
|
+
return [{ jsonrpc: '2.0', id, result: this.#symbols(message.params) }];
|
|
130
150
|
}
|
|
151
|
+
if (method === 'textDocument/formatting') {
|
|
152
|
+
return [{ jsonrpc: '2.0', id, result: this.#formatting(message.params) }];
|
|
153
|
+
}
|
|
154
|
+
if (method === 'shutdown')
|
|
155
|
+
return [{ jsonrpc: '2.0', id, result: null }];
|
|
131
156
|
if (id !== undefined && method !== undefined) {
|
|
132
157
|
return [
|
|
133
158
|
{
|
|
@@ -139,7 +164,6 @@ export class SyqlLanguageServer {
|
|
|
139
164
|
}
|
|
140
165
|
return [];
|
|
141
166
|
}
|
|
142
|
-
/** Reset cached manifest contexts (e.g. after schema edits). */
|
|
143
167
|
invalidateProjects() {
|
|
144
168
|
this.#contexts = new Map();
|
|
145
169
|
}
|
|
@@ -147,192 +171,335 @@ export class SyqlLanguageServer {
|
|
|
147
171
|
const path = uriToPath(uri);
|
|
148
172
|
const manifestDir = findManifestDir(path);
|
|
149
173
|
if (manifestDir === null)
|
|
150
|
-
return
|
|
174
|
+
return { kind: 'none' };
|
|
151
175
|
const cached = this.#contexts.get(manifestDir);
|
|
152
176
|
if (cached !== undefined)
|
|
153
177
|
return cached;
|
|
154
178
|
try {
|
|
155
179
|
const manifest = parseManifest(JSON.parse(readFileSync(resolve(manifestDir, MANIFEST_FILENAME), 'utf8')));
|
|
156
|
-
const
|
|
157
|
-
const ir = buildIr(manifest, migrations);
|
|
180
|
+
const ir = buildIr(manifest, loadMigrations(resolve(manifestDir, manifest.migrations)));
|
|
158
181
|
const { db } = makeQueryDb(ir);
|
|
159
182
|
const targets = ['ts'];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
if (manifest.output.swift !== undefined)
|
|
184
|
+
targets.push('swift');
|
|
185
|
+
if (manifest.output.kotlin !== undefined)
|
|
186
|
+
targets.push('kotlin');
|
|
187
|
+
if (manifest.output.dart !== undefined)
|
|
188
|
+
targets.push('dart');
|
|
189
|
+
const lookup = {
|
|
190
|
+
kind: 'ready',
|
|
191
|
+
context: {
|
|
192
|
+
ir,
|
|
193
|
+
db,
|
|
194
|
+
manifestDir,
|
|
195
|
+
queriesRoot: resolve(manifestDir, manifest.queries),
|
|
196
|
+
naming: {
|
|
197
|
+
naming: manifest.naming,
|
|
198
|
+
targets,
|
|
199
|
+
backend: manifest.queryBackend,
|
|
200
|
+
},
|
|
168
201
|
},
|
|
169
202
|
};
|
|
170
|
-
this.#contexts.set(manifestDir,
|
|
171
|
-
return
|
|
203
|
+
this.#contexts.set(manifestDir, lookup);
|
|
204
|
+
return lookup;
|
|
172
205
|
}
|
|
173
|
-
catch {
|
|
174
|
-
|
|
175
|
-
|
|
206
|
+
catch (error) {
|
|
207
|
+
const lookup = { kind: 'error', error };
|
|
208
|
+
this.#contexts.set(manifestDir, lookup);
|
|
209
|
+
return lookup;
|
|
176
210
|
}
|
|
177
211
|
}
|
|
212
|
+
#openText(file) {
|
|
213
|
+
for (const [uri, text] of this.#documents) {
|
|
214
|
+
if (resolve(uriToPath(uri)) === resolve(file))
|
|
215
|
+
return text;
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
#compilerView(uri, text) {
|
|
220
|
+
const file = resolve(uriToPath(uri));
|
|
221
|
+
const lookup = this.#context(uri);
|
|
222
|
+
if (lookup.kind === 'error')
|
|
223
|
+
throw lookup.error;
|
|
224
|
+
const project = lookup.kind === 'ready' ? lookup.context : undefined;
|
|
225
|
+
const root = project !== undefined && isWithin(project.queriesRoot, file)
|
|
226
|
+
? project.queriesRoot
|
|
227
|
+
: dirname(file);
|
|
228
|
+
const sourceByFile = new Map();
|
|
229
|
+
const entries = [];
|
|
230
|
+
if (project !== undefined && root === project.queriesRoot) {
|
|
231
|
+
for (const input of loadQueries(root)) {
|
|
232
|
+
if (!input.file.endsWith('.syql'))
|
|
233
|
+
continue;
|
|
234
|
+
const absolute = resolve(root, input.file);
|
|
235
|
+
sourceByFile.set(absolute, this.#openText(absolute) ?? input.sql);
|
|
236
|
+
entries.push(input.file);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
sourceByFile.set(file, text);
|
|
240
|
+
const currentEntry = relative(root, file).split(sep).join('/');
|
|
241
|
+
if (!entries.includes(currentEntry))
|
|
242
|
+
entries.push(currentEntry);
|
|
243
|
+
entries.sort();
|
|
244
|
+
const graph = buildSyqlModuleGraph(root, entries, (candidate) => {
|
|
245
|
+
const open = this.#openText(candidate);
|
|
246
|
+
if (open !== undefined)
|
|
247
|
+
return open;
|
|
248
|
+
const loaded = sourceByFile.get(candidate);
|
|
249
|
+
if (loaded !== undefined)
|
|
250
|
+
return loaded;
|
|
251
|
+
return existsSync(candidate)
|
|
252
|
+
? readFileSync(candidate, 'utf8')
|
|
253
|
+
: undefined;
|
|
254
|
+
});
|
|
255
|
+
const semantic = analyzeSyqlSemantics(graph);
|
|
256
|
+
const module = graph.moduleByPath.get(file);
|
|
257
|
+
if (module === undefined)
|
|
258
|
+
throw new TypegenError(file, 'current SYQL module missing');
|
|
259
|
+
const lowered = project === undefined
|
|
260
|
+
? []
|
|
261
|
+
: validateSyqlProgram(semantic, project.ir, project.db, project.naming).queries.map((query) => lowerSyqlQuery(query, project.ir, project.db, project.naming));
|
|
262
|
+
return { root, file, module, semantic, lowered };
|
|
263
|
+
}
|
|
178
264
|
#publishDiagnostics(uri) {
|
|
179
265
|
const text = this.#documents.get(uri) ?? '';
|
|
180
|
-
const diagnostics = this.#diagnose(uri, text);
|
|
181
266
|
return {
|
|
182
267
|
jsonrpc: '2.0',
|
|
183
268
|
method: 'textDocument/publishDiagnostics',
|
|
184
|
-
params: { uri, diagnostics },
|
|
269
|
+
params: { uri, diagnostics: this.#diagnose(uri, text) },
|
|
185
270
|
};
|
|
186
271
|
}
|
|
187
272
|
#diagnose(uri, text) {
|
|
188
|
-
const path = uriToPath(uri);
|
|
189
|
-
const context = this.#context(uri);
|
|
190
273
|
try {
|
|
191
|
-
|
|
192
|
-
// The full generate-time analysis: parse + lower + SQLite check.
|
|
193
|
-
const rel = relative(context.manifestDir, path).split('\\').join('/');
|
|
194
|
-
analyzeSyqlFile(rel, text, context.ir, context.db, context.naming);
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
// No manifest reachable: parser + lowering only.
|
|
198
|
-
const parsed = parseSyqlFile(path, text);
|
|
199
|
-
void parsed;
|
|
200
|
-
}
|
|
274
|
+
this.#compilerView(uri, text);
|
|
201
275
|
return [];
|
|
202
276
|
}
|
|
203
277
|
catch (error) {
|
|
204
|
-
return [this.#errorToDiagnostic(error, text)];
|
|
278
|
+
return [this.#errorToDiagnostic(error, text, resolve(uriToPath(uri)))];
|
|
205
279
|
}
|
|
206
280
|
}
|
|
207
|
-
#errorToDiagnostic(error, text) {
|
|
281
|
+
#errorToDiagnostic(error, text, file) {
|
|
208
282
|
const message = error instanceof Error ? error.message : String(error);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (lineMatch !== null) {
|
|
219
|
-
const line = Number.parseInt(lineMatch[1], 10) - 1;
|
|
220
|
-
range = {
|
|
221
|
-
start: { line, character: 0 },
|
|
222
|
-
end: { line, character: 200 },
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
else if (queryMatch !== null) {
|
|
226
|
-
try {
|
|
227
|
-
const parsed = parseSyqlFile('doc', text);
|
|
228
|
-
const decl = parsed.queries.find((q) => q.name === queryMatch[1]);
|
|
229
|
-
if (decl !== undefined) {
|
|
230
|
-
const start = offsetToPosition(text, decl.offset);
|
|
231
|
-
range = {
|
|
232
|
-
start,
|
|
233
|
-
end: { line: start.line, character: start.character + 200 },
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
// fall through to line 0
|
|
239
|
-
}
|
|
240
|
-
}
|
|
283
|
+
if (error instanceof SyqlFrontendError &&
|
|
284
|
+
resolve(error.sourceFile) === file) {
|
|
285
|
+
return {
|
|
286
|
+
range: spanRange(text, error.span),
|
|
287
|
+
severity: 1,
|
|
288
|
+
source: 'syncular',
|
|
289
|
+
code: error.code,
|
|
290
|
+
message,
|
|
291
|
+
};
|
|
241
292
|
}
|
|
242
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
range: {
|
|
295
|
+
start: { line: 0, character: 0 },
|
|
296
|
+
end: {
|
|
297
|
+
line: 0,
|
|
298
|
+
character: Math.max(1, text.split('\n')[0]?.length ?? 1),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
severity: 1,
|
|
302
|
+
source: 'syncular',
|
|
303
|
+
...(error instanceof SyqlFrontendError ? { code: error.code } : {}),
|
|
304
|
+
message: this.#context(pathToFileURL(file).href).kind === 'error'
|
|
305
|
+
? `SYQL9001_PROJECT_CONTEXT: ${message}`
|
|
306
|
+
: message,
|
|
307
|
+
};
|
|
243
308
|
}
|
|
244
|
-
#
|
|
245
|
-
const
|
|
246
|
-
const text = this.#documents.get(
|
|
309
|
+
#request(params) {
|
|
310
|
+
const request = params;
|
|
311
|
+
const text = this.#documents.get(request.textDocument.uri);
|
|
247
312
|
if (text === undefined)
|
|
248
313
|
return null;
|
|
249
|
-
const offset = positionToOffset(text,
|
|
314
|
+
const offset = positionToOffset(text, request.position);
|
|
250
315
|
const found = wordAt(text, offset);
|
|
251
316
|
if (found === null)
|
|
252
317
|
return null;
|
|
253
|
-
let parsed;
|
|
254
318
|
try {
|
|
255
|
-
|
|
319
|
+
return {
|
|
320
|
+
uri: request.textDocument.uri,
|
|
321
|
+
text,
|
|
322
|
+
offset,
|
|
323
|
+
found,
|
|
324
|
+
view: this.#compilerView(request.textDocument.uri, text),
|
|
325
|
+
};
|
|
256
326
|
}
|
|
257
327
|
catch {
|
|
258
328
|
return null;
|
|
259
329
|
}
|
|
260
|
-
|
|
330
|
+
}
|
|
331
|
+
#hover(params) {
|
|
332
|
+
const request = this.#request(params);
|
|
333
|
+
if (request === null)
|
|
334
|
+
return null;
|
|
335
|
+
const { found, view, offset } = request;
|
|
336
|
+
if (found.word === '@scope' || found.word === '@cover') {
|
|
337
|
+
return {
|
|
338
|
+
contents: {
|
|
339
|
+
kind: 'markdown',
|
|
340
|
+
value: found.word === '@scope'
|
|
341
|
+
? '`@scope` constructs exact reactive dependency keys and the same SQL predicate.'
|
|
342
|
+
: '`@cover` constructs exact dependencies plus checked window coverage.',
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
261
346
|
if (found.word.startsWith('@')) {
|
|
262
|
-
const
|
|
263
|
-
|
|
347
|
+
const predicate = view.semantic.predicateScopes
|
|
348
|
+
.get(view.file)
|
|
349
|
+
?.get(found.word.slice(1));
|
|
350
|
+
if (predicate === undefined)
|
|
264
351
|
return null;
|
|
265
352
|
return {
|
|
266
353
|
contents: {
|
|
267
354
|
kind: 'markdown',
|
|
268
|
-
value:
|
|
355
|
+
value: `predicate \`${predicate.declaration.name}\` from \`${relative(view.root, predicate.module.file)}\`\n\n\`\`\`sql\n${predicate.declaration.body.text.trim()}\n\`\`\``,
|
|
269
356
|
},
|
|
270
357
|
};
|
|
271
358
|
}
|
|
272
|
-
|
|
273
|
-
|
|
359
|
+
const query = view.module.queries.find((candidate) => offset >= candidate.span.start.offset &&
|
|
360
|
+
offset <= candidate.span.end.offset);
|
|
274
361
|
if (query !== undefined) {
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (analyzed === undefined)
|
|
281
|
-
return null;
|
|
282
|
-
const lines = [
|
|
283
|
-
'```sql',
|
|
284
|
-
analyzed.sql,
|
|
285
|
-
'```',
|
|
286
|
-
'',
|
|
287
|
-
`tables: ${analyzed.tables.join(', ')}`,
|
|
288
|
-
...(analyzed.variants !== undefined
|
|
289
|
-
? [`variants: ${analyzed.variants.length} enumerated statements`]
|
|
290
|
-
: []),
|
|
291
|
-
];
|
|
292
|
-
return { contents: { kind: 'markdown', value: lines.join('\n') } };
|
|
362
|
+
const parameter = query.parameters.find((candidate) => candidate.name === found.word);
|
|
363
|
+
if (parameter !== undefined) {
|
|
364
|
+
return {
|
|
365
|
+
contents: { kind: 'markdown', value: parameterHover(parameter) },
|
|
366
|
+
};
|
|
293
367
|
}
|
|
294
|
-
|
|
295
|
-
return
|
|
368
|
+
if (query.sort?.control === found.word) {
|
|
369
|
+
return {
|
|
370
|
+
contents: {
|
|
371
|
+
kind: 'markdown',
|
|
372
|
+
value: `sort control \`${found.word}\`; default profile \`${query.sort.defaultProfile}\``,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const profile = query.sort?.profiles.find((candidate) => candidate.name === found.word);
|
|
377
|
+
if (profile !== undefined) {
|
|
378
|
+
return {
|
|
379
|
+
contents: {
|
|
380
|
+
kind: 'markdown',
|
|
381
|
+
value: `sort profile \`${profile.name}\`\n\n\`\`\`sql\n${profile.order.text.trim()}\n\`\`\``,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
296
384
|
}
|
|
385
|
+
if (query.page?.control === found.word) {
|
|
386
|
+
return {
|
|
387
|
+
contents: {
|
|
388
|
+
kind: 'markdown',
|
|
389
|
+
value: `page control \`${found.word}\`: default ${query.page.defaultSize}, maximum ${query.page.maxSize}`,
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (query?.name === found.word) {
|
|
395
|
+
const lowered = view.lowered.find((candidate) => candidate.validated.logical.declaration === query);
|
|
396
|
+
if (lowered === undefined)
|
|
397
|
+
return null;
|
|
398
|
+
const defaultStatement = lowered.selected.statements.find((statement) => (statement.activationMask === undefined ||
|
|
399
|
+
statement.activationMask === 0) &&
|
|
400
|
+
(lowered.validated.sort === undefined ||
|
|
401
|
+
statement.sortProfile === lowered.validated.sort.defaultProfile));
|
|
402
|
+
return {
|
|
403
|
+
contents: {
|
|
404
|
+
kind: 'markdown',
|
|
405
|
+
value: [
|
|
406
|
+
`backend: **${lowered.selected.backend}** (${lowered.selected.statements.length} checked statement${lowered.selected.statements.length === 1 ? '' : 's'})`,
|
|
407
|
+
'',
|
|
408
|
+
'```sql',
|
|
409
|
+
defaultStatement?.sql ?? lowered.analysis.sql,
|
|
410
|
+
'```',
|
|
411
|
+
'',
|
|
412
|
+
`tables: ${lowered.analysis.tables.join(', ')}`,
|
|
413
|
+
].join('\n'),
|
|
414
|
+
},
|
|
415
|
+
};
|
|
297
416
|
}
|
|
298
417
|
return null;
|
|
299
418
|
}
|
|
300
419
|
#definition(params) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
if (text === undefined)
|
|
420
|
+
const request = this.#request(params);
|
|
421
|
+
if (request === null || !request.found.word.startsWith('@'))
|
|
304
422
|
return null;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
423
|
+
const predicate = request.view.semantic.predicateScopes
|
|
424
|
+
.get(request.view.file)
|
|
425
|
+
?.get(request.found.word.slice(1));
|
|
426
|
+
if (predicate === undefined)
|
|
308
427
|
return null;
|
|
309
|
-
|
|
428
|
+
const text = this.#openText(predicate.module.file) ?? predicate.module.source;
|
|
429
|
+
return {
|
|
430
|
+
uri: pathToFileURL(predicate.module.file).href,
|
|
431
|
+
range: spanRange(text, predicate.declaration.nameSpan),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
#references(params) {
|
|
435
|
+
const request = this.#request(params);
|
|
436
|
+
if (request === null || !request.found.word.startsWith('@'))
|
|
437
|
+
return [];
|
|
438
|
+
const target = request.view.semantic.predicateScopes
|
|
439
|
+
.get(request.view.file)
|
|
440
|
+
?.get(request.found.word.slice(1));
|
|
441
|
+
if (target === undefined)
|
|
442
|
+
return [];
|
|
443
|
+
const locations = [];
|
|
444
|
+
for (const module of request.view.semantic.graph.modules) {
|
|
445
|
+
const scope = request.view.semantic.predicateScopes.get(module.file);
|
|
446
|
+
for (const token of module.tokens) {
|
|
447
|
+
if (token.kind !== 'at-identifier')
|
|
448
|
+
continue;
|
|
449
|
+
const resolved = scope?.get(token.text.slice(1));
|
|
450
|
+
if (resolved?.id !== target.id)
|
|
451
|
+
continue;
|
|
452
|
+
locations.push({
|
|
453
|
+
uri: pathToFileURL(module.file).href,
|
|
454
|
+
range: spanRange(module.source, token.span),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return locations;
|
|
459
|
+
}
|
|
460
|
+
#symbols(params) {
|
|
461
|
+
const request = params;
|
|
462
|
+
const text = this.#documents.get(request.textDocument.uri);
|
|
463
|
+
if (text === undefined)
|
|
464
|
+
return [];
|
|
310
465
|
try {
|
|
311
|
-
parsed =
|
|
466
|
+
const parsed = parseSyqlSyntaxFile(uriToPath(request.textDocument.uri), text);
|
|
467
|
+
return parsed.declarations.map((declaration) => ({
|
|
468
|
+
name: declaration.name,
|
|
469
|
+
kind: declaration.kind === 'query' ? 12 : 12,
|
|
470
|
+
range: spanRange(text, declaration.span),
|
|
471
|
+
selectionRange: spanRange(text, declaration.nameSpan),
|
|
472
|
+
}));
|
|
312
473
|
}
|
|
313
474
|
catch {
|
|
314
|
-
return
|
|
475
|
+
return [];
|
|
315
476
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
477
|
+
}
|
|
478
|
+
#formatting(params) {
|
|
479
|
+
const request = params;
|
|
480
|
+
const text = this.#documents.get(request.textDocument.uri);
|
|
481
|
+
if (text === undefined)
|
|
482
|
+
return [];
|
|
483
|
+
try {
|
|
484
|
+
const formatted = formatSyql(uriToPath(request.textDocument.uri), text);
|
|
485
|
+
if (formatted === text)
|
|
486
|
+
return [];
|
|
487
|
+
return [
|
|
488
|
+
{
|
|
489
|
+
range: {
|
|
490
|
+
start: { line: 0, character: 0 },
|
|
491
|
+
end: offsetToPosition(text, text.length),
|
|
492
|
+
},
|
|
493
|
+
newText: formatted,
|
|
327
494
|
},
|
|
328
|
-
|
|
329
|
-
}
|
|
495
|
+
];
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return [];
|
|
499
|
+
}
|
|
330
500
|
}
|
|
331
501
|
}
|
|
332
|
-
|
|
333
|
-
// stdio transport (Content-Length framing)
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
/** Run the language server over stdio until `exit`. */
|
|
502
|
+
/** Run the LSP over Content-Length-framed JSON-RPC stdio. */
|
|
336
503
|
export async function runLspStdio() {
|
|
337
504
|
const server = new SyqlLanguageServer();
|
|
338
505
|
let buffer = Buffer.alloc(0);
|
|
@@ -356,21 +523,28 @@ export async function runLspStdio() {
|
|
|
356
523
|
continue;
|
|
357
524
|
}
|
|
358
525
|
const length = Number.parseInt(lengthMatch[1], 10);
|
|
359
|
-
|
|
526
|
+
const bodyStart = headerEnd + 4;
|
|
527
|
+
if (buffer.length < bodyStart + length)
|
|
360
528
|
break;
|
|
361
|
-
const body = buffer
|
|
362
|
-
|
|
363
|
-
|
|
529
|
+
const body = buffer
|
|
530
|
+
.subarray(bodyStart, bodyStart + length)
|
|
531
|
+
.toString('utf8');
|
|
532
|
+
buffer = buffer.subarray(bodyStart + length);
|
|
533
|
+
let incoming;
|
|
364
534
|
try {
|
|
365
|
-
|
|
535
|
+
incoming = JSON.parse(body);
|
|
366
536
|
}
|
|
367
537
|
catch {
|
|
538
|
+
write({
|
|
539
|
+
jsonrpc: '2.0',
|
|
540
|
+
error: { code: -32700, message: 'parse error' },
|
|
541
|
+
});
|
|
368
542
|
continue;
|
|
369
543
|
}
|
|
370
|
-
|
|
371
|
-
return;
|
|
372
|
-
for (const outgoing of server.handle(message))
|
|
544
|
+
for (const outgoing of server.handle(incoming))
|
|
373
545
|
write(outgoing);
|
|
546
|
+
if (incoming.method === 'exit')
|
|
547
|
+
return;
|
|
374
548
|
}
|
|
375
549
|
}
|
|
376
550
|
}
|