@syncular/typegen 0.5.1 → 0.7.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 +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -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 +349 -187
- 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 +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- 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 +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -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 +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -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 === 'boolean' ? 'bool' : type.base}${type.nullable ? ' | null' : ''}`;
|
|
82
|
+
}
|
|
83
|
+
function parameterHover(parameter) {
|
|
84
|
+
if (parameter.kind === 'range') {
|
|
85
|
+
return `${parameter.optional ? 'optional' : 'required'} inclusive range \`${parameter.name}: range<${typeText(parameter.type)}>\``;
|
|
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' : parameter.default === false ? 'default-false' : '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) }];
|
|
130
147
|
}
|
|
148
|
+
if (method === 'textDocument/documentSymbol') {
|
|
149
|
+
return [{ jsonrpc: '2.0', id, result: this.#symbols(message.params) }];
|
|
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,323 @@ 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;
|
|
210
|
+
}
|
|
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
|
+
}
|
|
176
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 };
|
|
177
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
330
|
+
}
|
|
331
|
+
#hover(params) {
|
|
332
|
+
const request = this.#request(params);
|
|
333
|
+
if (request === null)
|
|
334
|
+
return null;
|
|
335
|
+
const { found, view, offset } = request;
|
|
336
|
+
const predicate = view.semantic.predicateScopes
|
|
337
|
+
.get(view.file)
|
|
338
|
+
?.get(found.word);
|
|
339
|
+
if (predicate !== undefined) {
|
|
265
340
|
return {
|
|
266
341
|
contents: {
|
|
267
342
|
kind: 'markdown',
|
|
268
|
-
value:
|
|
343
|
+
value: `predicate \`${predicate.declaration.name}\` from \`${relative(view.root, predicate.module.file)}\`\n\n\`\`\`sql\n${predicate.declaration.body.text.trim()}\n\`\`\``,
|
|
269
344
|
},
|
|
270
345
|
};
|
|
271
346
|
}
|
|
272
|
-
|
|
273
|
-
|
|
347
|
+
const query = view.module.queries.find((candidate) => offset >= candidate.span.start.offset &&
|
|
348
|
+
offset <= candidate.span.end.offset);
|
|
274
349
|
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') } };
|
|
350
|
+
const parameter = query.parameters.find((candidate) => candidate.name === found.word);
|
|
351
|
+
if (parameter !== undefined) {
|
|
352
|
+
return {
|
|
353
|
+
contents: { kind: 'markdown', value: parameterHover(parameter) },
|
|
354
|
+
};
|
|
293
355
|
}
|
|
294
|
-
|
|
295
|
-
return
|
|
356
|
+
if (query.sort?.control === found.word) {
|
|
357
|
+
return {
|
|
358
|
+
contents: {
|
|
359
|
+
kind: 'markdown',
|
|
360
|
+
value: `sort control \`${found.word}\`; default profile \`${query.sort.defaultProfile}\``,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
const profile = query.sort?.profiles.find((candidate) => candidate.name === found.word);
|
|
365
|
+
if (profile !== undefined) {
|
|
366
|
+
return {
|
|
367
|
+
contents: {
|
|
368
|
+
kind: 'markdown',
|
|
369
|
+
value: `sort profile \`${profile.name}\`\n\n\`\`\`sql\n${profile.order.text.trim()}\n\`\`\``,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (query.limit?.control === found.word) {
|
|
374
|
+
return {
|
|
375
|
+
contents: {
|
|
376
|
+
kind: 'markdown',
|
|
377
|
+
value: `limit control \`${found.word}\`: default ${query.limit.defaultSize}, maximum ${query.limit.maxSize}`,
|
|
378
|
+
},
|
|
379
|
+
};
|
|
296
380
|
}
|
|
297
381
|
}
|
|
382
|
+
if (query?.name === found.word) {
|
|
383
|
+
const lowered = view.lowered.find((candidate) => candidate.validated.logical.declaration === query);
|
|
384
|
+
if (lowered === undefined)
|
|
385
|
+
return null;
|
|
386
|
+
const defaultStatement = lowered.selected.statements.find((statement) => (statement.activationMask === undefined ||
|
|
387
|
+
statement.activationMask === 0) &&
|
|
388
|
+
(lowered.validated.sort === undefined ||
|
|
389
|
+
statement.sortProfile === lowered.validated.sort.defaultProfile));
|
|
390
|
+
return {
|
|
391
|
+
contents: {
|
|
392
|
+
kind: 'markdown',
|
|
393
|
+
value: [
|
|
394
|
+
`backend: **${lowered.selected.backend}** (${lowered.selected.statements.length} checked statement${lowered.selected.statements.length === 1 ? '' : 's'})`,
|
|
395
|
+
'',
|
|
396
|
+
'```sql',
|
|
397
|
+
defaultStatement?.sql ?? lowered.analysis.sql,
|
|
398
|
+
'```',
|
|
399
|
+
'',
|
|
400
|
+
`tables: ${lowered.analysis.tables.join(', ')}`,
|
|
401
|
+
].join('\n'),
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
}
|
|
298
405
|
return null;
|
|
299
406
|
}
|
|
300
407
|
#definition(params) {
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
if (text === undefined)
|
|
408
|
+
const request = this.#request(params);
|
|
409
|
+
if (request === null)
|
|
304
410
|
return null;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
411
|
+
const predicate = request.view.semantic.predicateScopes
|
|
412
|
+
.get(request.view.file)
|
|
413
|
+
?.get(request.found.word);
|
|
414
|
+
if (predicate === undefined)
|
|
308
415
|
return null;
|
|
309
|
-
|
|
416
|
+
const text = this.#openText(predicate.module.file) ?? predicate.module.source;
|
|
417
|
+
return {
|
|
418
|
+
uri: pathToFileURL(predicate.module.file).href,
|
|
419
|
+
range: spanRange(text, predicate.declaration.nameSpan),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
#references(params) {
|
|
423
|
+
const request = this.#request(params);
|
|
424
|
+
if (request === null)
|
|
425
|
+
return [];
|
|
426
|
+
const target = request.view.semantic.predicateScopes
|
|
427
|
+
.get(request.view.file)
|
|
428
|
+
?.get(request.found.word);
|
|
429
|
+
if (target === undefined)
|
|
430
|
+
return [];
|
|
431
|
+
const locations = [];
|
|
432
|
+
for (const module of request.view.semantic.graph.modules) {
|
|
433
|
+
const scope = request.view.semantic.predicateScopes.get(module.file);
|
|
434
|
+
for (const token of module.tokens) {
|
|
435
|
+
if (token.kind !== 'identifier')
|
|
436
|
+
continue;
|
|
437
|
+
const resolved = scope?.get(token.text);
|
|
438
|
+
if (resolved?.id !== target.id)
|
|
439
|
+
continue;
|
|
440
|
+
locations.push({
|
|
441
|
+
uri: pathToFileURL(module.file).href,
|
|
442
|
+
range: spanRange(module.source, token.span),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return locations;
|
|
447
|
+
}
|
|
448
|
+
#symbols(params) {
|
|
449
|
+
const request = params;
|
|
450
|
+
const text = this.#documents.get(request.textDocument.uri);
|
|
451
|
+
if (text === undefined)
|
|
452
|
+
return [];
|
|
310
453
|
try {
|
|
311
|
-
parsed =
|
|
454
|
+
const parsed = parseSyqlSyntaxFile(uriToPath(request.textDocument.uri), text);
|
|
455
|
+
return parsed.declarations.map((declaration) => ({
|
|
456
|
+
name: declaration.name,
|
|
457
|
+
kind: declaration.kind === 'query' ? 12 : 12,
|
|
458
|
+
range: spanRange(text, declaration.span),
|
|
459
|
+
selectionRange: spanRange(text, declaration.nameSpan),
|
|
460
|
+
}));
|
|
312
461
|
}
|
|
313
462
|
catch {
|
|
314
|
-
return
|
|
463
|
+
return [];
|
|
315
464
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
465
|
+
}
|
|
466
|
+
#formatting(params) {
|
|
467
|
+
const request = params;
|
|
468
|
+
const text = this.#documents.get(request.textDocument.uri);
|
|
469
|
+
if (text === undefined)
|
|
470
|
+
return [];
|
|
471
|
+
try {
|
|
472
|
+
const formatted = formatSyql(uriToPath(request.textDocument.uri), text);
|
|
473
|
+
if (formatted === text)
|
|
474
|
+
return [];
|
|
475
|
+
return [
|
|
476
|
+
{
|
|
477
|
+
range: {
|
|
478
|
+
start: { line: 0, character: 0 },
|
|
479
|
+
end: offsetToPosition(text, text.length),
|
|
480
|
+
},
|
|
481
|
+
newText: formatted,
|
|
327
482
|
},
|
|
328
|
-
|
|
329
|
-
}
|
|
483
|
+
];
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return [];
|
|
487
|
+
}
|
|
330
488
|
}
|
|
331
489
|
}
|
|
332
|
-
|
|
333
|
-
// stdio transport (Content-Length framing)
|
|
334
|
-
// ---------------------------------------------------------------------------
|
|
335
|
-
/** Run the language server over stdio until `exit`. */
|
|
490
|
+
/** Run the LSP over Content-Length-framed JSON-RPC stdio. */
|
|
336
491
|
export async function runLspStdio() {
|
|
337
492
|
const server = new SyqlLanguageServer();
|
|
338
493
|
let buffer = Buffer.alloc(0);
|
|
@@ -356,21 +511,28 @@ export async function runLspStdio() {
|
|
|
356
511
|
continue;
|
|
357
512
|
}
|
|
358
513
|
const length = Number.parseInt(lengthMatch[1], 10);
|
|
359
|
-
|
|
514
|
+
const bodyStart = headerEnd + 4;
|
|
515
|
+
if (buffer.length < bodyStart + length)
|
|
360
516
|
break;
|
|
361
|
-
const body = buffer
|
|
362
|
-
|
|
363
|
-
|
|
517
|
+
const body = buffer
|
|
518
|
+
.subarray(bodyStart, bodyStart + length)
|
|
519
|
+
.toString('utf8');
|
|
520
|
+
buffer = buffer.subarray(bodyStart + length);
|
|
521
|
+
let incoming;
|
|
364
522
|
try {
|
|
365
|
-
|
|
523
|
+
incoming = JSON.parse(body);
|
|
366
524
|
}
|
|
367
525
|
catch {
|
|
526
|
+
write({
|
|
527
|
+
jsonrpc: '2.0',
|
|
528
|
+
error: { code: -32700, message: 'parse error' },
|
|
529
|
+
});
|
|
368
530
|
continue;
|
|
369
531
|
}
|
|
370
|
-
|
|
371
|
-
return;
|
|
372
|
-
for (const outgoing of server.handle(message))
|
|
532
|
+
for (const outgoing of server.handle(incoming))
|
|
373
533
|
write(outgoing);
|
|
534
|
+
if (incoming.method === 'exit')
|
|
535
|
+
return;
|
|
374
536
|
}
|
|
375
537
|
}
|
|
376
538
|
}
|