@514labs/moose-lsp 1.3.12000141 → 1.3.12000161
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.
|
@@ -23,40 +23,64 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
));
|
|
24
24
|
var import_node_assert = __toESM(require("node:assert"));
|
|
25
25
|
var import_node_child_process = require("node:child_process");
|
|
26
|
+
var fs = __toESM(require("node:fs/promises"));
|
|
27
|
+
var os = __toESM(require("node:os"));
|
|
26
28
|
var path = __toESM(require("node:path"));
|
|
27
29
|
var import_node_test = require("node:test");
|
|
30
|
+
const CompletionItemKind = {
|
|
31
|
+
Function: 3,
|
|
32
|
+
Keyword: 14,
|
|
33
|
+
TypeParameter: 25,
|
|
34
|
+
Class: 7,
|
|
35
|
+
Constant: 21,
|
|
36
|
+
Property: 10,
|
|
37
|
+
Method: 2
|
|
38
|
+
};
|
|
28
39
|
class LspClient {
|
|
29
40
|
constructor(serverPath2) {
|
|
30
|
-
this.buffer =
|
|
41
|
+
this.buffer = Buffer.alloc(0);
|
|
31
42
|
this.messages = [];
|
|
32
43
|
this.nextId = 1;
|
|
44
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
45
|
+
this.notificationListeners = [];
|
|
46
|
+
this.stderrChunks = [];
|
|
33
47
|
this.process = (0, import_node_child_process.spawn)("node", [serverPath2, "--stdio"], {
|
|
34
48
|
stdio: ["pipe", "pipe", "pipe"]
|
|
35
49
|
});
|
|
36
50
|
this.process.stdout?.on("data", (data) => {
|
|
37
|
-
this.buffer
|
|
51
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
38
52
|
this.parseMessages();
|
|
39
53
|
});
|
|
40
|
-
this.process.stderr?.on("data", (
|
|
54
|
+
this.process.stderr?.on("data", (data) => {
|
|
55
|
+
this.stderrChunks.push(data.toString());
|
|
41
56
|
});
|
|
42
57
|
}
|
|
43
58
|
parseMessages() {
|
|
44
59
|
while (true) {
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
const
|
|
48
|
-
const match =
|
|
60
|
+
const headerEndIndex = this.buffer.indexOf("\r\n\r\n");
|
|
61
|
+
if (headerEndIndex === -1) break;
|
|
62
|
+
const headerStr = this.buffer.subarray(0, headerEndIndex).toString("utf-8");
|
|
63
|
+
const match = headerStr.match(/Content-Length:\s*(\d+)/i);
|
|
49
64
|
if (!match) break;
|
|
50
65
|
const contentLength = parseInt(match[1], 10);
|
|
51
|
-
const contentStart =
|
|
66
|
+
const contentStart = headerEndIndex + 4;
|
|
52
67
|
const contentEnd = contentStart + contentLength;
|
|
53
68
|
if (this.buffer.length < contentEnd) break;
|
|
54
|
-
const content = this.buffer.
|
|
55
|
-
this.buffer = this.buffer.
|
|
69
|
+
const content = this.buffer.subarray(contentStart, contentEnd).toString("utf-8");
|
|
70
|
+
this.buffer = this.buffer.subarray(contentEnd);
|
|
56
71
|
const message = JSON.parse(content);
|
|
57
72
|
this.messages.push(message);
|
|
58
|
-
if (
|
|
59
|
-
this.
|
|
73
|
+
if (message.id !== void 0) {
|
|
74
|
+
const pending = this.pendingRequests.get(message.id);
|
|
75
|
+
if (pending) {
|
|
76
|
+
this.pendingRequests.delete(message.id);
|
|
77
|
+
pending.resolve(message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (message.method && message.id === void 0) {
|
|
81
|
+
for (const listener of this.notificationListeners) {
|
|
82
|
+
listener(message);
|
|
83
|
+
}
|
|
60
84
|
}
|
|
61
85
|
}
|
|
62
86
|
}
|
|
@@ -67,53 +91,872 @@ class LspClient {
|
|
|
67
91
|
`;
|
|
68
92
|
this.process.stdin?.write(header + content);
|
|
69
93
|
}
|
|
70
|
-
async request(method, params, timeoutMs =
|
|
94
|
+
async request(method, params, timeoutMs = 1e4) {
|
|
71
95
|
const id = this.nextId++;
|
|
72
96
|
return new Promise((resolve, reject) => {
|
|
73
97
|
const timeout = setTimeout(() => {
|
|
74
|
-
this.
|
|
75
|
-
reject(
|
|
98
|
+
this.pendingRequests.delete(id);
|
|
99
|
+
reject(
|
|
100
|
+
new Error(`Timeout waiting for response to ${method} (id=${id})`)
|
|
101
|
+
);
|
|
76
102
|
}, timeoutMs);
|
|
77
|
-
this.onMessage = (msg) => {
|
|
78
|
-
if (msg.id === id) {
|
|
79
|
-
clearTimeout(timeout);
|
|
80
|
-
this.onMessage = void 0;
|
|
81
|
-
resolve(msg);
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
103
|
const existing = this.messages.find((m) => m.id === id);
|
|
85
104
|
if (existing) {
|
|
86
105
|
clearTimeout(timeout);
|
|
87
|
-
this.onMessage = void 0;
|
|
88
106
|
resolve(existing);
|
|
89
107
|
return;
|
|
90
108
|
}
|
|
109
|
+
this.pendingRequests.set(id, {
|
|
110
|
+
resolve: (msg) => {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
resolve(msg);
|
|
113
|
+
},
|
|
114
|
+
reject: (err) => {
|
|
115
|
+
clearTimeout(timeout);
|
|
116
|
+
reject(err);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
91
119
|
this.send({ jsonrpc: "2.0", id, method, params });
|
|
92
120
|
});
|
|
93
121
|
}
|
|
94
122
|
notify(method, params) {
|
|
95
123
|
this.send({ jsonrpc: "2.0", method, params });
|
|
96
124
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
openDocument(uri, languageId, text) {
|
|
126
|
+
this.notify("textDocument/didOpen", {
|
|
127
|
+
textDocument: { uri, languageId, version: 1, text }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
changeDocument(uri, version, text) {
|
|
131
|
+
this.notify("textDocument/didChange", {
|
|
132
|
+
textDocument: { uri, version },
|
|
133
|
+
contentChanges: [{ text }]
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
saveDocument(uri) {
|
|
137
|
+
this.notify("textDocument/didSave", {
|
|
138
|
+
textDocument: { uri }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
waitForDiagnostics(uri, timeoutMs = 1e4) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const deadline = setTimeout(() => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(new Error(`Timeout waiting for diagnostics for ${uri}`));
|
|
146
|
+
}, timeoutMs);
|
|
147
|
+
const listener = (msg) => {
|
|
148
|
+
if (msg.method !== "textDocument/publishDiagnostics") return;
|
|
149
|
+
const params = msg.params;
|
|
150
|
+
if (params.uri === uri) {
|
|
151
|
+
cleanup();
|
|
152
|
+
resolve(params.diagnostics);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
const cleanup = () => {
|
|
156
|
+
clearTimeout(deadline);
|
|
157
|
+
const idx = this.notificationListeners.indexOf(listener);
|
|
158
|
+
if (idx !== -1) this.notificationListeners.splice(idx, 1);
|
|
159
|
+
};
|
|
160
|
+
for (let i = this.messages.length - 1; i >= 0; i--) {
|
|
161
|
+
const m = this.messages[i];
|
|
162
|
+
if (m.method === "textDocument/publishDiagnostics") {
|
|
163
|
+
const params = m.params;
|
|
164
|
+
if (params.uri === uri) {
|
|
165
|
+
clearTimeout(deadline);
|
|
166
|
+
resolve(params.diagnostics);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
102
170
|
}
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
171
|
+
this.notificationListeners.push(listener);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Waits for fresh diagnostics that arrive after this call.
|
|
176
|
+
* Ignores any diagnostics already in the message buffer.
|
|
177
|
+
*/
|
|
178
|
+
waitForFreshDiagnostics(uri, timeoutMs = 1e4) {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const deadline = setTimeout(() => {
|
|
181
|
+
cleanup();
|
|
182
|
+
reject(new Error(`Timeout waiting for fresh diagnostics for ${uri}`));
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
const listener = (msg) => {
|
|
185
|
+
if (msg.method !== "textDocument/publishDiagnostics") return;
|
|
186
|
+
const params = msg.params;
|
|
187
|
+
if (params.uri === uri) {
|
|
188
|
+
cleanup();
|
|
189
|
+
resolve(params.diagnostics);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const cleanup = () => {
|
|
193
|
+
clearTimeout(deadline);
|
|
194
|
+
const idx = this.notificationListeners.indexOf(listener);
|
|
195
|
+
if (idx !== -1) this.notificationListeners.splice(idx, 1);
|
|
196
|
+
};
|
|
197
|
+
this.notificationListeners.push(listener);
|
|
198
|
+
});
|
|
106
199
|
}
|
|
107
200
|
getMessages() {
|
|
108
201
|
return [...this.messages];
|
|
109
202
|
}
|
|
203
|
+
getStderr() {
|
|
204
|
+
return this.stderrChunks.join("");
|
|
205
|
+
}
|
|
206
|
+
getLogMessages() {
|
|
207
|
+
return this.messages.filter((m) => m.method === "window/logMessage").map((m) => m.params.message);
|
|
208
|
+
}
|
|
110
209
|
close() {
|
|
111
210
|
this.process.kill();
|
|
112
211
|
}
|
|
113
212
|
}
|
|
114
213
|
const serverPath = path.join(__dirname, "..", "dist", "server.js");
|
|
115
|
-
|
|
116
|
-
|
|
214
|
+
function cursorAfter(fileContent, searchText) {
|
|
215
|
+
const idx = fileContent.indexOf(searchText);
|
|
216
|
+
if (idx === -1) {
|
|
217
|
+
throw new Error(`cursorAfter: "${searchText}" not found in content`);
|
|
218
|
+
}
|
|
219
|
+
const endIdx = idx + searchText.length;
|
|
220
|
+
const before2 = fileContent.slice(0, endIdx);
|
|
221
|
+
const lines = before2.split("\n");
|
|
222
|
+
return {
|
|
223
|
+
line: lines.length - 1,
|
|
224
|
+
character: lines[lines.length - 1].length
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function initializeClient(client, rootUri, snippetSupport = true) {
|
|
228
|
+
const response = await client.request("initialize", {
|
|
229
|
+
processId: null,
|
|
230
|
+
rootUri,
|
|
231
|
+
capabilities: {
|
|
232
|
+
textDocument: {
|
|
233
|
+
completion: {
|
|
234
|
+
completionItem: {
|
|
235
|
+
snippetSupport
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
synchronization: {
|
|
239
|
+
didSave: true
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
client.notify("initialized", {});
|
|
245
|
+
return response;
|
|
246
|
+
}
|
|
247
|
+
const TS_TEST_FILE_CONTENT = `import { sql } from '@514labs/moose-lib';
|
|
248
|
+
|
|
249
|
+
// Valid SQL \u2014 should produce no diagnostics
|
|
250
|
+
const valid = sql\`SELECT count() FROM users\`;
|
|
251
|
+
|
|
252
|
+
// Invalid SQL \u2014 should produce an error diagnostic
|
|
253
|
+
const invalid = sql\`SELCT * FROM users\`;
|
|
254
|
+
|
|
255
|
+
// Completions: default context (cursor after "SELECT ")
|
|
256
|
+
const comp1 = sql\`SELECT \`;
|
|
257
|
+
|
|
258
|
+
// Context-aware: ENGINE =
|
|
259
|
+
const engine = sql\`CREATE TABLE t ENGINE = \`;
|
|
260
|
+
|
|
261
|
+
// Context-aware: FORMAT
|
|
262
|
+
const format = sql\`SELECT * FORMAT \`;
|
|
263
|
+
|
|
264
|
+
// Context-aware: SETTINGS
|
|
265
|
+
const settings = sql\`SELECT * SETTINGS \`;
|
|
266
|
+
|
|
267
|
+
// Context-aware: column definition (data types)
|
|
268
|
+
const coldef = sql\`CREATE TABLE t (id \`;
|
|
269
|
+
|
|
270
|
+
// Context-aware: FROM (table functions)
|
|
271
|
+
const fromCtx = sql\`SELECT * FROM \`;
|
|
272
|
+
|
|
273
|
+
// Hover targets
|
|
274
|
+
const hover = sql\`SELECT count() FROM users WHERE toUInt32(id) > 0\`;
|
|
275
|
+
|
|
276
|
+
// Format SQL target (lowercase)
|
|
277
|
+
const fmt = sql\`select * from users where id = 1\`;
|
|
278
|
+
|
|
279
|
+
// Combinators
|
|
280
|
+
const comb = sql\`SELECT sumIf(amount, active), countIf(status) FROM orders\`;
|
|
281
|
+
|
|
282
|
+
// Prefix filtering
|
|
283
|
+
const prefix = sql\`SELECT cou\`;
|
|
284
|
+
|
|
285
|
+
// Combinator prefix filtering
|
|
286
|
+
const combPrefix = sql\`SELECT sum\`;
|
|
287
|
+
|
|
288
|
+
// Default context (empty SQL \u2014 triggers Default completions with all kinds)
|
|
289
|
+
const defaultCtx = sql\`\`;
|
|
290
|
+
`;
|
|
291
|
+
async function createTsFixture() {
|
|
292
|
+
const tmpDir = await fs.mkdtemp(
|
|
293
|
+
path.join(os.tmpdir(), "moose-lsp-integ-ts-")
|
|
294
|
+
);
|
|
295
|
+
await fs.writeFile(
|
|
296
|
+
path.join(tmpDir, "package.json"),
|
|
297
|
+
JSON.stringify({
|
|
298
|
+
name: "test-moose-ts",
|
|
299
|
+
dependencies: { "@514labs/moose-lib": "*" }
|
|
300
|
+
})
|
|
301
|
+
);
|
|
302
|
+
await fs.writeFile(
|
|
303
|
+
path.join(tmpDir, "tsconfig.json"),
|
|
304
|
+
JSON.stringify({
|
|
305
|
+
compilerOptions: {
|
|
306
|
+
target: "ES2020",
|
|
307
|
+
module: "commonjs",
|
|
308
|
+
strict: true,
|
|
309
|
+
esModuleInterop: true,
|
|
310
|
+
moduleResolution: "node"
|
|
311
|
+
},
|
|
312
|
+
include: ["app/**/*.ts"]
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
const mooseLibDir = path.join(
|
|
316
|
+
tmpDir,
|
|
317
|
+
"node_modules",
|
|
318
|
+
"@514labs",
|
|
319
|
+
"moose-lib"
|
|
320
|
+
);
|
|
321
|
+
await fs.mkdir(mooseLibDir, { recursive: true });
|
|
322
|
+
await fs.writeFile(
|
|
323
|
+
path.join(mooseLibDir, "package.json"),
|
|
324
|
+
JSON.stringify({
|
|
325
|
+
name: "@514labs/moose-lib",
|
|
326
|
+
main: "index.js",
|
|
327
|
+
types: "index.d.ts"
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
await fs.writeFile(
|
|
331
|
+
path.join(mooseLibDir, "index.js"),
|
|
332
|
+
'module.exports.sql = function sql() { return ""; };\n'
|
|
333
|
+
);
|
|
334
|
+
await fs.writeFile(
|
|
335
|
+
path.join(mooseLibDir, "index.d.ts"),
|
|
336
|
+
"export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): string;\n"
|
|
337
|
+
);
|
|
338
|
+
const appDir = path.join(tmpDir, "app");
|
|
339
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
340
|
+
await fs.writeFile(path.join(appDir, "test.ts"), TS_TEST_FILE_CONTENT);
|
|
341
|
+
return tmpDir;
|
|
342
|
+
}
|
|
343
|
+
const PY_TEST_FILE_CONTENT = `from moose_lib import sql
|
|
344
|
+
|
|
345
|
+
# Invalid SQL
|
|
346
|
+
query = sql("SELCT * FROM users")
|
|
347
|
+
`;
|
|
348
|
+
async function createPyFixture() {
|
|
349
|
+
const tmpDir = await fs.mkdtemp(
|
|
350
|
+
path.join(os.tmpdir(), "moose-lsp-integ-py-")
|
|
351
|
+
);
|
|
352
|
+
await fs.writeFile(
|
|
353
|
+
path.join(tmpDir, "pyproject.toml"),
|
|
354
|
+
`[project]
|
|
355
|
+
name = "test-moose-py"
|
|
356
|
+
dependencies = ["moose-lib"]
|
|
357
|
+
`
|
|
358
|
+
);
|
|
359
|
+
const appDir = path.join(tmpDir, "app");
|
|
360
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
361
|
+
await fs.writeFile(path.join(appDir, "test.py"), PY_TEST_FILE_CONTENT);
|
|
362
|
+
return tmpDir;
|
|
363
|
+
}
|
|
364
|
+
async function createTsFixtureWithDockerCompose(chVersion) {
|
|
365
|
+
const tmpDir = await createTsFixture();
|
|
366
|
+
await fs.writeFile(
|
|
367
|
+
path.join(tmpDir, "docker-compose.dev.override.yaml"),
|
|
368
|
+
`services:
|
|
369
|
+
clickhousedb:
|
|
370
|
+
image: clickhouse/clickhouse-server:${chVersion}
|
|
371
|
+
`
|
|
372
|
+
);
|
|
373
|
+
return tmpDir;
|
|
374
|
+
}
|
|
375
|
+
(0, import_node_test.describe)("TypeScript LSP features", () => {
|
|
376
|
+
let client;
|
|
377
|
+
let tmpDir;
|
|
378
|
+
const tsFileUri = () => `file://${path.join(tmpDir, "app", "test.ts")}`;
|
|
379
|
+
(0, import_node_test.before)(async () => {
|
|
380
|
+
tmpDir = await createTsFixture();
|
|
381
|
+
client = new LspClient(serverPath);
|
|
382
|
+
await initializeClient(client, `file://${tmpDir}`);
|
|
383
|
+
client.openDocument(tsFileUri(), "typescript", TS_TEST_FILE_CONTENT);
|
|
384
|
+
await client.waitForDiagnostics(tsFileUri());
|
|
385
|
+
});
|
|
386
|
+
(0, import_node_test.after)(async () => {
|
|
387
|
+
client.close();
|
|
388
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
389
|
+
});
|
|
390
|
+
(0, import_node_test.test)("valid SQL produces no error diagnostics on that template", async () => {
|
|
391
|
+
const validLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
392
|
+
(l) => l.includes("const valid")
|
|
393
|
+
);
|
|
394
|
+
const diagnostics = await client.waitForDiagnostics(tsFileUri());
|
|
395
|
+
const onValidLine = diagnostics.filter(
|
|
396
|
+
(d) => d.range.start.line === validLine
|
|
397
|
+
);
|
|
398
|
+
import_node_assert.default.strictEqual(
|
|
399
|
+
onValidLine.length,
|
|
400
|
+
0,
|
|
401
|
+
`Expected no diagnostics on the valid SQL line, got: ${JSON.stringify(onValidLine)}`
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
(0, import_node_test.test)("invalid SQL produces an error diagnostic", async () => {
|
|
405
|
+
const diagnostics = await client.waitForDiagnostics(tsFileUri());
|
|
406
|
+
const invalidLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
407
|
+
(l) => l.includes("const invalid")
|
|
408
|
+
);
|
|
409
|
+
const onInvalidLine = diagnostics.filter(
|
|
410
|
+
(d) => d.range.start.line === invalidLine
|
|
411
|
+
);
|
|
412
|
+
import_node_assert.default.ok(
|
|
413
|
+
onInvalidLine.length > 0,
|
|
414
|
+
`Expected at least one diagnostic on the invalid SQL line (${invalidLine})`
|
|
415
|
+
);
|
|
416
|
+
const diag = onInvalidLine[0];
|
|
417
|
+
import_node_assert.default.strictEqual(diag.severity, 1, "Severity should be Error (1)");
|
|
418
|
+
import_node_assert.default.strictEqual(diag.source, "moose-sql");
|
|
419
|
+
});
|
|
420
|
+
(0, import_node_test.test)("fixing SQL clears diagnostic on that line", async () => {
|
|
421
|
+
const invalidLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
422
|
+
(l) => l.includes("const invalid")
|
|
423
|
+
);
|
|
424
|
+
const fixedContent = TS_TEST_FILE_CONTENT.replace(
|
|
425
|
+
"SELCT * FROM users",
|
|
426
|
+
"SELECT * FROM users"
|
|
427
|
+
);
|
|
428
|
+
client.changeDocument(tsFileUri(), 2, fixedContent);
|
|
429
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
430
|
+
client.saveDocument(tsFileUri());
|
|
431
|
+
const deadline = Date.now() + 1e4;
|
|
432
|
+
let lastDiagnostics = [];
|
|
433
|
+
while (Date.now() < deadline) {
|
|
434
|
+
lastDiagnostics = await client.waitForFreshDiagnostics(tsFileUri());
|
|
435
|
+
const onFixedLine2 = lastDiagnostics.filter(
|
|
436
|
+
(d) => d.range.start.line === invalidLine && d.source === "moose-sql"
|
|
437
|
+
);
|
|
438
|
+
if (onFixedLine2.length === 0) break;
|
|
439
|
+
}
|
|
440
|
+
const onFixedLine = lastDiagnostics.filter(
|
|
441
|
+
(d) => d.range.start.line === invalidLine && d.source === "moose-sql"
|
|
442
|
+
);
|
|
443
|
+
import_node_assert.default.strictEqual(
|
|
444
|
+
onFixedLine.length,
|
|
445
|
+
0,
|
|
446
|
+
`Expected no diagnostic on the fixed line (${invalidLine}), got: ${JSON.stringify(onFixedLine)}`
|
|
447
|
+
);
|
|
448
|
+
client.changeDocument(tsFileUri(), 3, TS_TEST_FILE_CONTENT);
|
|
449
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
450
|
+
client.saveDocument(tsFileUri());
|
|
451
|
+
await client.waitForFreshDiagnostics(tsFileUri());
|
|
452
|
+
});
|
|
453
|
+
(0, import_node_test.test)("completions inside SQL template returns items", async () => {
|
|
454
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "comp1 = sql`SELECT ");
|
|
455
|
+
const response = await client.request("textDocument/completion", {
|
|
456
|
+
textDocument: { uri: tsFileUri() },
|
|
457
|
+
position: pos
|
|
458
|
+
});
|
|
459
|
+
const items = response.result;
|
|
460
|
+
import_node_assert.default.ok(Array.isArray(items), "Result should be an array");
|
|
461
|
+
import_node_assert.default.ok(items.length > 0, "Should have completion items");
|
|
462
|
+
});
|
|
463
|
+
(0, import_node_test.test)("SelectClause context returns only functions", async () => {
|
|
464
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "comp1 = sql`SELECT ");
|
|
465
|
+
const response = await client.request("textDocument/completion", {
|
|
466
|
+
textDocument: { uri: tsFileUri() },
|
|
467
|
+
position: pos
|
|
468
|
+
});
|
|
469
|
+
const items = response.result;
|
|
470
|
+
const kinds = new Set(items.map((i) => i.kind));
|
|
471
|
+
import_node_assert.default.ok(
|
|
472
|
+
kinds.has(CompletionItemKind.Function) || kinds.has(CompletionItemKind.Method),
|
|
473
|
+
"Should include function or method completions"
|
|
474
|
+
);
|
|
475
|
+
for (const item of items) {
|
|
476
|
+
import_node_assert.default.ok(
|
|
477
|
+
item.kind === CompletionItemKind.Function || item.kind === CompletionItemKind.Method,
|
|
478
|
+
`SelectClause should only have functions, got kind=${item.kind} for "${item.label}"`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
(0, import_node_test.test)("completions outside SQL template returns empty", async () => {
|
|
483
|
+
const response = await client.request("textDocument/completion", {
|
|
484
|
+
textDocument: { uri: tsFileUri() },
|
|
485
|
+
position: { line: 0, character: 5 }
|
|
486
|
+
});
|
|
487
|
+
const items = response.result;
|
|
488
|
+
import_node_assert.default.ok(Array.isArray(items), "Result should be an array");
|
|
489
|
+
import_node_assert.default.strictEqual(
|
|
490
|
+
items.length,
|
|
491
|
+
0,
|
|
492
|
+
"Should have no completions outside template"
|
|
493
|
+
);
|
|
494
|
+
});
|
|
495
|
+
(0, import_node_test.test)("prefix filtering narrows completions", async () => {
|
|
496
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "prefix = sql`SELECT cou");
|
|
497
|
+
const response = await client.request("textDocument/completion", {
|
|
498
|
+
textDocument: { uri: tsFileUri() },
|
|
499
|
+
position: pos
|
|
500
|
+
});
|
|
501
|
+
const items = response.result;
|
|
502
|
+
import_node_assert.default.ok(items.length > 0, "Should have some completions");
|
|
503
|
+
const labels = items.map((i) => i.label.toLowerCase());
|
|
504
|
+
import_node_assert.default.ok(
|
|
505
|
+
labels.some((l) => l.startsWith("count")),
|
|
506
|
+
`Should include count*, got: ${labels.slice(0, 10).join(", ")}`
|
|
507
|
+
);
|
|
508
|
+
for (const item of items) {
|
|
509
|
+
import_node_assert.default.ok(
|
|
510
|
+
item.label.toLowerCase().startsWith("cou"),
|
|
511
|
+
`Item "${item.label}" should start with "cou"`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
(0, import_node_test.test)("ENGINE = context returns table engines", async () => {
|
|
516
|
+
const pos = cursorAfter(
|
|
517
|
+
TS_TEST_FILE_CONTENT,
|
|
518
|
+
"engine = sql`CREATE TABLE t ENGINE = "
|
|
519
|
+
);
|
|
520
|
+
const response = await client.request("textDocument/completion", {
|
|
521
|
+
textDocument: { uri: tsFileUri() },
|
|
522
|
+
position: pos
|
|
523
|
+
});
|
|
524
|
+
const items = response.result;
|
|
525
|
+
import_node_assert.default.ok(items.length > 0, "Should have engine completions");
|
|
526
|
+
const labels = items.map((i) => i.label);
|
|
527
|
+
import_node_assert.default.ok(
|
|
528
|
+
labels.some((l) => l === "MergeTree"),
|
|
529
|
+
`Should include MergeTree, got: ${labels.slice(0, 10).join(", ")}`
|
|
530
|
+
);
|
|
531
|
+
for (const item of items) {
|
|
532
|
+
import_node_assert.default.strictEqual(
|
|
533
|
+
item.kind,
|
|
534
|
+
CompletionItemKind.Class,
|
|
535
|
+
`Engine item "${item.label}" should have kind=Class(${CompletionItemKind.Class}), got ${item.kind}`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
(0, import_node_test.test)("FORMAT context returns formats", async () => {
|
|
540
|
+
const pos = cursorAfter(
|
|
541
|
+
TS_TEST_FILE_CONTENT,
|
|
542
|
+
"format = sql`SELECT * FORMAT "
|
|
543
|
+
);
|
|
544
|
+
const response = await client.request("textDocument/completion", {
|
|
545
|
+
textDocument: { uri: tsFileUri() },
|
|
546
|
+
position: pos
|
|
547
|
+
});
|
|
548
|
+
const items = response.result;
|
|
549
|
+
import_node_assert.default.ok(items.length > 0, "Should have format completions");
|
|
550
|
+
const labels = items.map((i) => i.label);
|
|
551
|
+
import_node_assert.default.ok(
|
|
552
|
+
labels.some((l) => l === "JSON"),
|
|
553
|
+
`Should include JSON, got: ${labels.slice(0, 10).join(", ")}`
|
|
554
|
+
);
|
|
555
|
+
for (const item of items) {
|
|
556
|
+
import_node_assert.default.strictEqual(
|
|
557
|
+
item.kind,
|
|
558
|
+
CompletionItemKind.Constant,
|
|
559
|
+
`Format item "${item.label}" should have kind=Constant(${CompletionItemKind.Constant}), got ${item.kind}`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
(0, import_node_test.test)("SETTINGS context returns settings", async () => {
|
|
564
|
+
const pos = cursorAfter(
|
|
565
|
+
TS_TEST_FILE_CONTENT,
|
|
566
|
+
"settings = sql`SELECT * SETTINGS "
|
|
567
|
+
);
|
|
568
|
+
const response = await client.request("textDocument/completion", {
|
|
569
|
+
textDocument: { uri: tsFileUri() },
|
|
570
|
+
position: pos
|
|
571
|
+
});
|
|
572
|
+
const items = response.result;
|
|
573
|
+
import_node_assert.default.ok(items.length > 0, "Should have settings completions");
|
|
574
|
+
const labels = items.map((i) => i.label);
|
|
575
|
+
import_node_assert.default.ok(
|
|
576
|
+
labels.some((l) => l === "max_threads"),
|
|
577
|
+
`Should include max_threads, got: ${labels.slice(0, 15).join(", ")}`
|
|
578
|
+
);
|
|
579
|
+
for (const item of items) {
|
|
580
|
+
import_node_assert.default.strictEqual(
|
|
581
|
+
item.kind,
|
|
582
|
+
CompletionItemKind.Property,
|
|
583
|
+
`Setting item "${item.label}" should have kind=Property(${CompletionItemKind.Property}), got ${item.kind}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
(0, import_node_test.test)("column definition context returns data types", async () => {
|
|
588
|
+
const pos = cursorAfter(
|
|
589
|
+
TS_TEST_FILE_CONTENT,
|
|
590
|
+
"coldef = sql`CREATE TABLE t (id "
|
|
591
|
+
);
|
|
592
|
+
const response = await client.request("textDocument/completion", {
|
|
593
|
+
textDocument: { uri: tsFileUri() },
|
|
594
|
+
position: pos
|
|
595
|
+
});
|
|
596
|
+
const items = response.result;
|
|
597
|
+
import_node_assert.default.ok(items.length > 0, "Should have data type completions");
|
|
598
|
+
const labels = items.map((i) => i.label);
|
|
599
|
+
import_node_assert.default.ok(
|
|
600
|
+
labels.some((l) => l === "UInt64"),
|
|
601
|
+
`Should include UInt64, got: ${labels.slice(0, 15).join(", ")}`
|
|
602
|
+
);
|
|
603
|
+
import_node_assert.default.ok(
|
|
604
|
+
labels.some((l) => l === "String"),
|
|
605
|
+
`Should include String, got: ${labels.slice(0, 15).join(", ")}`
|
|
606
|
+
);
|
|
607
|
+
});
|
|
608
|
+
(0, import_node_test.test)("FROM context returns table functions", async () => {
|
|
609
|
+
const pos = cursorAfter(
|
|
610
|
+
TS_TEST_FILE_CONTENT,
|
|
611
|
+
"fromCtx = sql`SELECT * FROM "
|
|
612
|
+
);
|
|
613
|
+
const response = await client.request("textDocument/completion", {
|
|
614
|
+
textDocument: { uri: tsFileUri() },
|
|
615
|
+
position: pos
|
|
616
|
+
});
|
|
617
|
+
const items = response.result;
|
|
618
|
+
import_node_assert.default.ok(items.length > 0, "Should have FROM completions");
|
|
619
|
+
const labels = items.map((i) => i.label);
|
|
620
|
+
import_node_assert.default.ok(
|
|
621
|
+
labels.some((l) => l === "file" || l === "url"),
|
|
622
|
+
`Should include table functions like file/url, got: ${labels.slice(0, 15).join(", ")}`
|
|
623
|
+
);
|
|
624
|
+
});
|
|
625
|
+
(0, import_node_test.test)("hover on known function shows documentation", async () => {
|
|
626
|
+
const hoverLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
627
|
+
(l) => l.includes("sql`SELECT count()")
|
|
628
|
+
);
|
|
629
|
+
const lineText = TS_TEST_FILE_CONTENT.split("\n")[hoverLine];
|
|
630
|
+
const countCharIdx = lineText.indexOf("count");
|
|
631
|
+
import_node_assert.default.ok(countCharIdx !== -1, "Should find count in hover line");
|
|
632
|
+
const response = await client.request("textDocument/hover", {
|
|
633
|
+
textDocument: { uri: tsFileUri() },
|
|
634
|
+
position: { line: hoverLine, character: countCharIdx + 1 }
|
|
635
|
+
});
|
|
636
|
+
import_node_assert.default.ok(response.result, "Hover should return a result for count");
|
|
637
|
+
const hover = response.result;
|
|
638
|
+
import_node_assert.default.ok(
|
|
639
|
+
hover.contents.value.length > 0,
|
|
640
|
+
"Hover content should not be empty"
|
|
641
|
+
);
|
|
642
|
+
import_node_assert.default.strictEqual(
|
|
643
|
+
hover.contents.kind,
|
|
644
|
+
"markdown",
|
|
645
|
+
"Hover should be markdown"
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
(0, import_node_test.test)("hover on keyword shows documentation", async () => {
|
|
649
|
+
const hoverLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
650
|
+
(l) => l.includes("sql`SELECT count()")
|
|
651
|
+
);
|
|
652
|
+
const lineText = TS_TEST_FILE_CONTENT.split("\n")[hoverLine];
|
|
653
|
+
const selectIdx = lineText.indexOf("SELECT");
|
|
654
|
+
const response = await client.request("textDocument/hover", {
|
|
655
|
+
textDocument: { uri: tsFileUri() },
|
|
656
|
+
position: { line: hoverLine, character: selectIdx + 1 }
|
|
657
|
+
});
|
|
658
|
+
import_node_assert.default.ok(
|
|
659
|
+
response.result,
|
|
660
|
+
"Hover should return a result for SELECT keyword"
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
(0, import_node_test.test)("hover outside SQL template returns null", async () => {
|
|
664
|
+
const response = await client.request("textDocument/hover", {
|
|
665
|
+
textDocument: { uri: tsFileUri() },
|
|
666
|
+
position: { line: 0, character: 5 }
|
|
667
|
+
});
|
|
668
|
+
import_node_assert.default.strictEqual(
|
|
669
|
+
response.result,
|
|
670
|
+
null,
|
|
671
|
+
"Hover outside template should be null"
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
(0, import_node_test.test)("hover on unknown word returns null", async () => {
|
|
675
|
+
const hoverLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
676
|
+
(l) => l.includes("sql`SELECT count() FROM users")
|
|
677
|
+
);
|
|
678
|
+
const lineText = TS_TEST_FILE_CONTENT.split("\n")[hoverLine];
|
|
679
|
+
const usersIdx = lineText.indexOf("users");
|
|
680
|
+
const response = await client.request("textDocument/hover", {
|
|
681
|
+
textDocument: { uri: tsFileUri() },
|
|
682
|
+
position: { line: hoverLine, character: usersIdx + 1 }
|
|
683
|
+
});
|
|
684
|
+
import_node_assert.default.strictEqual(
|
|
685
|
+
response.result,
|
|
686
|
+
null,
|
|
687
|
+
"Hover on unknown word should be null"
|
|
688
|
+
);
|
|
689
|
+
});
|
|
690
|
+
(0, import_node_test.test)("Format SQL action is offered for valid lowercase SQL", async () => {
|
|
691
|
+
const fmtLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
692
|
+
(l) => l.includes("sql`select * from users")
|
|
693
|
+
);
|
|
694
|
+
const response = await client.request("textDocument/codeAction", {
|
|
695
|
+
textDocument: { uri: tsFileUri() },
|
|
696
|
+
range: {
|
|
697
|
+
start: { line: fmtLine, character: 15 },
|
|
698
|
+
end: { line: fmtLine, character: 15 }
|
|
699
|
+
},
|
|
700
|
+
context: { diagnostics: [] }
|
|
701
|
+
});
|
|
702
|
+
const actions = response.result;
|
|
703
|
+
import_node_assert.default.ok(Array.isArray(actions), "Code actions should be an array");
|
|
704
|
+
const formatAction = actions.find((a) => a.title.includes("Format"));
|
|
705
|
+
import_node_assert.default.ok(formatAction, "Should offer a Format SQL action");
|
|
706
|
+
});
|
|
707
|
+
(0, import_node_test.test)("Format SQL produces uppercase keywords", async () => {
|
|
708
|
+
const fmtLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
709
|
+
(l) => l.includes("sql`select * from users")
|
|
710
|
+
);
|
|
711
|
+
const response = await client.request("textDocument/codeAction", {
|
|
712
|
+
textDocument: { uri: tsFileUri() },
|
|
713
|
+
range: {
|
|
714
|
+
start: { line: fmtLine, character: 15 },
|
|
715
|
+
end: { line: fmtLine, character: 15 }
|
|
716
|
+
},
|
|
717
|
+
context: { diagnostics: [] }
|
|
718
|
+
});
|
|
719
|
+
const actions = response.result;
|
|
720
|
+
const formatAction = actions.find((a) => a.title.includes("Format"));
|
|
721
|
+
import_node_assert.default.ok(formatAction?.edit, "Format action should have an edit");
|
|
722
|
+
const edits = Object.values(
|
|
723
|
+
formatAction.edit.changes
|
|
724
|
+
).flat();
|
|
725
|
+
import_node_assert.default.ok(edits.length > 0, "Should have at least one text edit");
|
|
726
|
+
const newText = edits[0].newText;
|
|
727
|
+
import_node_assert.default.ok(
|
|
728
|
+
newText.includes("SELECT") && newText.includes("FROM"),
|
|
729
|
+
`Formatted SQL should contain uppercase keywords, got: ${newText}`
|
|
730
|
+
);
|
|
731
|
+
});
|
|
732
|
+
(0, import_node_test.test)("Format SQL not offered for invalid SQL", async () => {
|
|
733
|
+
const invalidLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
734
|
+
(l) => l.includes("sql`SELCT * FROM users")
|
|
735
|
+
);
|
|
736
|
+
const response = await client.request("textDocument/codeAction", {
|
|
737
|
+
textDocument: { uri: tsFileUri() },
|
|
738
|
+
range: {
|
|
739
|
+
start: { line: invalidLine, character: 15 },
|
|
740
|
+
end: { line: invalidLine, character: 15 }
|
|
741
|
+
},
|
|
742
|
+
context: { diagnostics: [] }
|
|
743
|
+
});
|
|
744
|
+
const actions = response.result;
|
|
745
|
+
const formatAction = (actions || []).find(
|
|
746
|
+
(a) => a.title.includes("Format")
|
|
747
|
+
);
|
|
748
|
+
import_node_assert.default.ok(!formatAction, "Should NOT offer Format SQL for invalid SQL");
|
|
749
|
+
});
|
|
750
|
+
(0, import_node_test.test)('sumIf appears in completions after "sum" prefix', async () => {
|
|
751
|
+
const pos = cursorAfter(
|
|
752
|
+
TS_TEST_FILE_CONTENT,
|
|
753
|
+
"combPrefix = sql`SELECT sum"
|
|
754
|
+
);
|
|
755
|
+
const response = await client.request("textDocument/completion", {
|
|
756
|
+
textDocument: { uri: tsFileUri() },
|
|
757
|
+
position: pos
|
|
758
|
+
});
|
|
759
|
+
const items = response.result;
|
|
760
|
+
const labels = items.map((i) => i.label);
|
|
761
|
+
import_node_assert.default.ok(
|
|
762
|
+
labels.some((l) => l === "sumIf"),
|
|
763
|
+
`Should include sumIf combinator, got: ${labels.slice(0, 20).join(", ")}`
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
(0, import_node_test.test)("combinator function has documentation", async () => {
|
|
767
|
+
const pos = cursorAfter(
|
|
768
|
+
TS_TEST_FILE_CONTENT,
|
|
769
|
+
"combPrefix = sql`SELECT sum"
|
|
770
|
+
);
|
|
771
|
+
const response = await client.request("textDocument/completion", {
|
|
772
|
+
textDocument: { uri: tsFileUri() },
|
|
773
|
+
position: pos
|
|
774
|
+
});
|
|
775
|
+
const items = response.result;
|
|
776
|
+
const sumIf = items.find((i) => i.label === "sumIf");
|
|
777
|
+
import_node_assert.default.ok(sumIf, "Should have sumIf item");
|
|
778
|
+
import_node_assert.default.ok(sumIf.documentation, "sumIf should have documentation");
|
|
779
|
+
const docValue = typeof sumIf.documentation === "string" ? sumIf.documentation : sumIf.documentation.value;
|
|
780
|
+
import_node_assert.default.ok(
|
|
781
|
+
docValue.toLowerCase().includes("combinator") || docValue.toLowerCase().includes("aggregate") || docValue.toLowerCase().includes("if"),
|
|
782
|
+
`sumIf docs should mention combinator/aggregate, got: ${docValue.slice(0, 200)}`
|
|
783
|
+
);
|
|
784
|
+
});
|
|
785
|
+
(0, import_node_test.test)("hover on combinator function shows info", async () => {
|
|
786
|
+
const combLine = TS_TEST_FILE_CONTENT.split("\n").findIndex(
|
|
787
|
+
(l) => l.includes("countIf(status)")
|
|
788
|
+
);
|
|
789
|
+
const lineText = TS_TEST_FILE_CONTENT.split("\n")[combLine];
|
|
790
|
+
const countIfIdx = lineText.indexOf("countIf");
|
|
791
|
+
const response = await client.request("textDocument/hover", {
|
|
792
|
+
textDocument: { uri: tsFileUri() },
|
|
793
|
+
position: { line: combLine, character: countIfIdx + 1 }
|
|
794
|
+
});
|
|
795
|
+
import_node_assert.default.ok(response.result, "Hover should return info for countIf");
|
|
796
|
+
const hover = response.result;
|
|
797
|
+
import_node_assert.default.ok(
|
|
798
|
+
hover.contents.value.length > 0,
|
|
799
|
+
"countIf hover should have content"
|
|
800
|
+
);
|
|
801
|
+
});
|
|
802
|
+
(0, import_node_test.test)("GROUP BY appears in default context completions", async () => {
|
|
803
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "defaultCtx = sql`");
|
|
804
|
+
const response = await client.request("textDocument/completion", {
|
|
805
|
+
textDocument: { uri: tsFileUri() },
|
|
806
|
+
position: pos
|
|
807
|
+
});
|
|
808
|
+
const items = response.result;
|
|
809
|
+
const labels = items.map((i) => i.label);
|
|
810
|
+
import_node_assert.default.ok(
|
|
811
|
+
labels.some((l) => l === "GROUP BY"),
|
|
812
|
+
`Should include "GROUP BY" keyword, got sample: ${labels.slice(0, 30).join(", ")}`
|
|
813
|
+
);
|
|
814
|
+
});
|
|
815
|
+
(0, import_node_test.test)("ORDER BY appears in default context completions", async () => {
|
|
816
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "defaultCtx = sql`");
|
|
817
|
+
const response = await client.request("textDocument/completion", {
|
|
818
|
+
textDocument: { uri: tsFileUri() },
|
|
819
|
+
position: pos
|
|
820
|
+
});
|
|
821
|
+
const items = response.result;
|
|
822
|
+
const labels = items.map((i) => i.label);
|
|
823
|
+
import_node_assert.default.ok(
|
|
824
|
+
labels.some((l) => l === "ORDER BY"),
|
|
825
|
+
`Should include "ORDER BY" keyword, got sample: ${labels.slice(0, 30).join(", ")}`
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
(0, import_node_test.describe)("Snippet support toggle", () => {
|
|
830
|
+
(0, import_node_test.test)("snippets enabled: function completion has snippet format", async () => {
|
|
831
|
+
const tmpDir = await createTsFixture();
|
|
832
|
+
const client = new LspClient(serverPath);
|
|
833
|
+
try {
|
|
834
|
+
await initializeClient(client, `file://${tmpDir}`, true);
|
|
835
|
+
const uri = `file://${path.join(tmpDir, "app", "test.ts")}`;
|
|
836
|
+
client.openDocument(uri, "typescript", TS_TEST_FILE_CONTENT);
|
|
837
|
+
await client.waitForDiagnostics(uri);
|
|
838
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "prefix = sql`SELECT cou");
|
|
839
|
+
const response = await client.request("textDocument/completion", {
|
|
840
|
+
textDocument: { uri },
|
|
841
|
+
position: pos
|
|
842
|
+
});
|
|
843
|
+
const items = response.result;
|
|
844
|
+
const countItem = items.find((i) => i.label === "count");
|
|
845
|
+
import_node_assert.default.ok(
|
|
846
|
+
countItem,
|
|
847
|
+
`Should have count completion, got: ${items.map((i) => i.label).slice(0, 10).join(", ")}`
|
|
848
|
+
);
|
|
849
|
+
import_node_assert.default.strictEqual(
|
|
850
|
+
countItem.insertText,
|
|
851
|
+
"count($1)$0",
|
|
852
|
+
"Snippet mode should produce snippet insertText"
|
|
853
|
+
);
|
|
854
|
+
import_node_assert.default.strictEqual(
|
|
855
|
+
countItem.insertTextFormat,
|
|
856
|
+
2,
|
|
857
|
+
"insertTextFormat should be Snippet (2)"
|
|
858
|
+
);
|
|
859
|
+
} finally {
|
|
860
|
+
client.close();
|
|
861
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
(0, import_node_test.test)("snippets disabled: function completion has plain format", async () => {
|
|
865
|
+
const tmpDir = await createTsFixture();
|
|
866
|
+
const client = new LspClient(serverPath);
|
|
867
|
+
try {
|
|
868
|
+
await initializeClient(client, `file://${tmpDir}`, false);
|
|
869
|
+
const uri = `file://${path.join(tmpDir, "app", "test.ts")}`;
|
|
870
|
+
client.openDocument(uri, "typescript", TS_TEST_FILE_CONTENT);
|
|
871
|
+
await client.waitForDiagnostics(uri);
|
|
872
|
+
const pos = cursorAfter(TS_TEST_FILE_CONTENT, "prefix = sql`SELECT cou");
|
|
873
|
+
const response = await client.request("textDocument/completion", {
|
|
874
|
+
textDocument: { uri },
|
|
875
|
+
position: pos
|
|
876
|
+
});
|
|
877
|
+
const items = response.result;
|
|
878
|
+
const countItem = items.find((i) => i.label === "count");
|
|
879
|
+
import_node_assert.default.ok(
|
|
880
|
+
countItem,
|
|
881
|
+
`Should have count completion, got: ${items.map((i) => i.label).slice(0, 10).join(", ")}`
|
|
882
|
+
);
|
|
883
|
+
import_node_assert.default.strictEqual(
|
|
884
|
+
countItem.insertText,
|
|
885
|
+
"count()",
|
|
886
|
+
"Non-snippet mode should produce plain insertText"
|
|
887
|
+
);
|
|
888
|
+
import_node_assert.default.strictEqual(
|
|
889
|
+
countItem.insertTextFormat,
|
|
890
|
+
1,
|
|
891
|
+
"insertTextFormat should be PlainText (1)"
|
|
892
|
+
);
|
|
893
|
+
} finally {
|
|
894
|
+
client.close();
|
|
895
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
896
|
+
}
|
|
897
|
+
});
|
|
898
|
+
});
|
|
899
|
+
(0, import_node_test.describe)("ClickHouse version detection", () => {
|
|
900
|
+
(0, import_node_test.test)("detects version from docker-compose", async () => {
|
|
901
|
+
const tmpDir = await createTsFixtureWithDockerCompose("25.6");
|
|
902
|
+
const client = new LspClient(serverPath);
|
|
903
|
+
try {
|
|
904
|
+
await initializeClient(client, `file://${tmpDir}`);
|
|
905
|
+
const logs = client.getLogMessages();
|
|
906
|
+
import_node_assert.default.ok(
|
|
907
|
+
logs.some((l) => l.includes("Detected ClickHouse version: 25.6")),
|
|
908
|
+
`Should detect version 25.6. Logs: ${logs.join(" | ")}`
|
|
909
|
+
);
|
|
910
|
+
} finally {
|
|
911
|
+
client.close();
|
|
912
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
(0, import_node_test.test)("falls back to latest when no docker-compose", async () => {
|
|
916
|
+
const tmpDir = await createTsFixture();
|
|
917
|
+
const client = new LspClient(serverPath);
|
|
918
|
+
try {
|
|
919
|
+
await initializeClient(client, `file://${tmpDir}`);
|
|
920
|
+
const logs = client.getLogMessages();
|
|
921
|
+
import_node_assert.default.ok(
|
|
922
|
+
logs.some(
|
|
923
|
+
(l) => l.includes("No ClickHouse version detected, using latest") || l.includes("using latest")
|
|
924
|
+
),
|
|
925
|
+
`Should fall back to latest. Logs: ${logs.join(" | ")}`
|
|
926
|
+
);
|
|
927
|
+
} finally {
|
|
928
|
+
client.close();
|
|
929
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
(0, import_node_test.describe)("Python LSP features", () => {
|
|
934
|
+
(0, import_node_test.test)("Python invalid SQL produces a diagnostic", async () => {
|
|
935
|
+
const tmpDir = await createPyFixture();
|
|
936
|
+
const client = new LspClient(serverPath);
|
|
937
|
+
try {
|
|
938
|
+
await initializeClient(client, `file://${tmpDir}`);
|
|
939
|
+
const pyUri = `file://${path.join(tmpDir, "app", "test.py")}`;
|
|
940
|
+
client.openDocument(pyUri, "python", PY_TEST_FILE_CONTENT);
|
|
941
|
+
const diagnostics = await client.waitForDiagnostics(pyUri);
|
|
942
|
+
import_node_assert.default.ok(
|
|
943
|
+
diagnostics.length > 0,
|
|
944
|
+
`Expected diagnostics for invalid Python SQL, got none`
|
|
945
|
+
);
|
|
946
|
+
import_node_assert.default.strictEqual(
|
|
947
|
+
diagnostics[0].severity,
|
|
948
|
+
1,
|
|
949
|
+
"Should be Error severity"
|
|
950
|
+
);
|
|
951
|
+
import_node_assert.default.strictEqual(diagnostics[0].source, "moose-sql");
|
|
952
|
+
} finally {
|
|
953
|
+
client.close();
|
|
954
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
955
|
+
}
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
(0, import_node_test.describe)("Server basic protocol", () => {
|
|
959
|
+
(0, import_node_test.test)("responds to initialize request", async () => {
|
|
117
960
|
const client = new LspClient(serverPath);
|
|
118
961
|
try {
|
|
119
962
|
const response = await client.request("initialize", {
|
|
@@ -132,7 +975,7 @@ const serverPath = path.join(__dirname, "..", "dist", "server.js");
|
|
|
132
975
|
client.close();
|
|
133
976
|
}
|
|
134
977
|
});
|
|
135
|
-
|
|
978
|
+
(0, import_node_test.test)("handles didSave notification and logs message", async () => {
|
|
136
979
|
const client = new LspClient(serverPath);
|
|
137
980
|
try {
|
|
138
981
|
await client.request("initialize", {
|
|
@@ -148,39 +991,24 @@ const serverPath = path.join(__dirname, "..", "dist", "server.js");
|
|
|
148
991
|
});
|
|
149
992
|
client.notify("initialized", {});
|
|
150
993
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
151
|
-
client.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
text: "const x = 1;"
|
|
157
|
-
}
|
|
158
|
-
});
|
|
994
|
+
client.openDocument(
|
|
995
|
+
"file:///tmp/test-project/app/test.ts",
|
|
996
|
+
"typescript",
|
|
997
|
+
"const x = 1;"
|
|
998
|
+
);
|
|
159
999
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
160
|
-
client.
|
|
161
|
-
|
|
162
|
-
|
|
1000
|
+
client.saveDocument("file:///tmp/test-project/app/test.ts");
|
|
1001
|
+
const deadline = Date.now() + 3e3;
|
|
1002
|
+
let found = false;
|
|
1003
|
+
while (Date.now() < deadline) {
|
|
1004
|
+
const logs = client.getLogMessages();
|
|
1005
|
+
if (logs.some((l) => l.includes("didSave received"))) {
|
|
1006
|
+
found = true;
|
|
1007
|
+
break;
|
|
163
1008
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
if (m.method !== "window/logMessage") return false;
|
|
168
|
-
const params = m.params;
|
|
169
|
-
return params.message.includes("didSave received");
|
|
170
|
-
});
|
|
171
|
-
}, 3e3);
|
|
172
|
-
const messages = client.getMessages();
|
|
173
|
-
const logMessages = messages.filter(
|
|
174
|
-
(m) => m.method === "window/logMessage"
|
|
175
|
-
);
|
|
176
|
-
const didSaveLog = logMessages.find((m) => {
|
|
177
|
-
const params = m.params;
|
|
178
|
-
return params.message.includes("didSave received");
|
|
179
|
-
});
|
|
180
|
-
import_node_assert.default.ok(
|
|
181
|
-
didSaveLog,
|
|
182
|
-
`Should log didSave received message. Got: ${JSON.stringify(logMessages.map((m) => m.params.message))}`
|
|
183
|
-
);
|
|
1009
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1010
|
+
}
|
|
1011
|
+
import_node_assert.default.ok(found, "Should log didSave received message");
|
|
184
1012
|
} finally {
|
|
185
1013
|
client.close();
|
|
186
1014
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/server.integration.test.ts"],"sourcesContent":["import assert from 'node:assert';\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport * as path from 'node:path';\nimport { test } from 'node:test';\n\ninterface LspMessage {\n jsonrpc: '2.0';\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: unknown;\n}\n\nclass LspClient {\n private process: ChildProcess;\n private buffer = '';\n private messages: LspMessage[] = [];\n private nextId = 1;\n private onMessage?: (msg: LspMessage) => void;\n\n constructor(serverPath: string) {\n this.process = spawn('node', [serverPath, '--stdio'], {\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n this.process.stdout?.on('data', (data: Buffer) => {\n this.buffer += data.toString();\n this.parseMessages();\n });\n\n this.process.stderr?.on('data', (_data: Buffer) => {\n // Uncomment for debugging:\n // console.error('LSP stderr:', _data.toString());\n });\n }\n\n private parseMessages() {\n while (true) {\n const headerEnd = this.buffer.indexOf('\\r\\n\\r\\n');\n if (headerEnd === -1) break;\n\n const header = this.buffer.slice(0, headerEnd);\n const match = header.match(/Content-Length: (\\d+)/);\n if (!match) break;\n\n const contentLength = parseInt(match[1], 10);\n const contentStart = headerEnd + 4;\n const contentEnd = contentStart + contentLength;\n\n if (this.buffer.length < contentEnd) break;\n\n const content = this.buffer.slice(contentStart, contentEnd);\n this.buffer = this.buffer.slice(contentEnd);\n\n const message = JSON.parse(content) as LspMessage;\n this.messages.push(message);\n\n if (this.onMessage) {\n this.onMessage(message);\n }\n }\n }\n\n send(message: LspMessage): void {\n const content = JSON.stringify(message);\n const header = `Content-Length: ${Buffer.byteLength(content)}\\r\\n\\r\\n`;\n this.process.stdin?.write(header + content);\n }\n\n async request(\n method: string,\n params: unknown,\n timeoutMs = 5000,\n ): Promise<LspMessage> {\n const id = this.nextId++;\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.onMessage = undefined;\n reject(new Error(`Timeout waiting for response to ${method}`));\n }, timeoutMs);\n\n this.onMessage = (msg) => {\n if (msg.id === id) {\n clearTimeout(timeout);\n this.onMessage = undefined;\n resolve(msg);\n }\n };\n\n // Check if response already arrived\n const existing = this.messages.find((m) => m.id === id);\n if (existing) {\n clearTimeout(timeout);\n this.onMessage = undefined;\n resolve(existing);\n return;\n }\n\n this.send({ jsonrpc: '2.0', id, method, params });\n });\n }\n\n notify(method: string, params: unknown): void {\n this.send({ jsonrpc: '2.0', method, params });\n }\n\n async waitForCondition(\n predicate: (messages: LspMessage[]) => boolean,\n timeoutMs = 5000,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n if (predicate(this.messages)) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n throw new Error('Timeout waiting for condition');\n }\n\n getMessages(): LspMessage[] {\n return [...this.messages];\n }\n\n close(): void {\n this.process.kill();\n }\n}\n\nconst serverPath = path.join(__dirname, '..', 'dist', 'server.js');\n\ntest('Server Integration Tests', async (t) => {\n await t.test('responds to initialize request', async () => {\n const client = new LspClient(serverPath);\n\n try {\n const response = await client.request('initialize', {\n processId: null,\n rootUri: 'file:///tmp/test-project',\n capabilities: {},\n });\n\n assert.ok(response.result, 'Should have result');\n const result = response.result as {\n capabilities: { textDocumentSync: unknown };\n };\n assert.ok(result.capabilities, 'Should have capabilities');\n assert.ok(\n result.capabilities.textDocumentSync,\n 'Should have textDocumentSync capability',\n );\n } finally {\n client.close();\n }\n });\n\n await t.test('handles didSave notification and logs message', async () => {\n const client = new LspClient(serverPath);\n\n try {\n // Initialize\n await client.request('initialize', {\n processId: null,\n rootUri: 'file:///tmp/test-project',\n capabilities: {\n textDocument: {\n synchronization: {\n didSave: true,\n },\n },\n },\n });\n\n // Send initialized\n client.notify('initialized', {});\n\n // Wait a bit for server to be ready\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n // Must send didOpen before didSave (TextDocuments requires this)\n client.notify('textDocument/didOpen', {\n textDocument: {\n uri: 'file:///tmp/test-project/app/test.ts',\n languageId: 'typescript',\n version: 1,\n text: 'const x = 1;',\n },\n });\n\n await new Promise((resolve) => setTimeout(resolve, 50));\n\n // Send didSave\n client.notify('textDocument/didSave', {\n textDocument: {\n uri: 'file:///tmp/test-project/app/test.ts',\n },\n });\n\n // Wait for the didSave log message\n await client.waitForCondition((messages) => {\n return messages.some((m) => {\n if (m.method !== 'window/logMessage') return false;\n const params = m.params as { message: string };\n return params.message.includes('didSave received');\n });\n }, 3000);\n\n // Verify\n const messages = client.getMessages();\n const logMessages = messages.filter(\n (m) => m.method === 'window/logMessage',\n );\n const didSaveLog = logMessages.find((m) => {\n const params = m.params as { message: string };\n return params.message.includes('didSave received');\n });\n\n assert.ok(\n didSaveLog,\n `Should log didSave received message. Got: ${JSON.stringify(logMessages.map((m) => (m.params as { message: string }).message))}`,\n );\n } finally {\n client.close();\n }\n });\n});\n"],"mappings":"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","names":["serverPath","assert","messages"]}
|
|
1
|
+
{"version":3,"sources":["../src/server.integration.test.ts"],"sourcesContent":["import assert from 'node:assert';\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport * as fs from 'node:fs/promises';\nimport * as os from 'node:os';\nimport * as path from 'node:path';\nimport { after, before, describe, test } from 'node:test';\n\n// ---------------------------------------------------------------------------\n// LSP message types\n// ---------------------------------------------------------------------------\n\ninterface LspMessage {\n jsonrpc: '2.0';\n id?: number;\n method?: string;\n params?: unknown;\n result?: unknown;\n error?: unknown;\n}\n\ninterface LspDiagnostic {\n range: {\n start: { line: number; character: number };\n end: { line: number; character: number };\n };\n severity: number;\n message: string;\n source?: string;\n}\n\ninterface LspCompletionItem {\n label: string;\n kind?: number;\n detail?: string;\n documentation?: { kind: string; value: string } | string;\n insertText?: string;\n insertTextFormat?: number;\n sortText?: string;\n}\n\n// LSP CompletionItemKind constants\nconst CompletionItemKind = {\n Function: 3,\n Keyword: 14,\n TypeParameter: 25,\n Class: 7,\n Constant: 21,\n Property: 10,\n Method: 2,\n} as const;\n\n// ---------------------------------------------------------------------------\n// LspClient — improved with Buffer-based parsing and concurrent request support\n// ---------------------------------------------------------------------------\n\nclass LspClient {\n private process: ChildProcess;\n private buffer = Buffer.alloc(0);\n private messages: LspMessage[] = [];\n private nextId = 1;\n private pendingRequests = new Map<\n number,\n { resolve: (msg: LspMessage) => void; reject: (err: Error) => void }\n >();\n private notificationListeners: Array<(msg: LspMessage) => void> = [];\n private stderrChunks: string[] = [];\n\n constructor(serverPath: string) {\n this.process = spawn('node', [serverPath, '--stdio'], {\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n this.process.stdout?.on('data', (data: Buffer) => {\n this.buffer = Buffer.concat([this.buffer, data]);\n this.parseMessages();\n });\n\n this.process.stderr?.on('data', (data: Buffer) => {\n this.stderrChunks.push(data.toString());\n });\n }\n\n private parseMessages() {\n while (true) {\n const headerEndIndex = this.buffer.indexOf('\\r\\n\\r\\n');\n if (headerEndIndex === -1) break;\n\n const headerStr = this.buffer\n .subarray(0, headerEndIndex)\n .toString('utf-8');\n const match = headerStr.match(/Content-Length:\\s*(\\d+)/i);\n if (!match) break;\n\n const contentLength = parseInt(match[1], 10);\n const contentStart = headerEndIndex + 4;\n const contentEnd = contentStart + contentLength;\n\n if (this.buffer.length < contentEnd) break;\n\n const content = this.buffer\n .subarray(contentStart, contentEnd)\n .toString('utf-8');\n this.buffer = this.buffer.subarray(contentEnd);\n\n const message = JSON.parse(content) as LspMessage;\n this.messages.push(message);\n\n // Resolve pending request if this is a response\n if (message.id !== undefined) {\n const pending = this.pendingRequests.get(message.id);\n if (pending) {\n this.pendingRequests.delete(message.id);\n pending.resolve(message);\n }\n }\n\n // Notify listeners for server-initiated messages (notifications)\n if (message.method && message.id === undefined) {\n for (const listener of this.notificationListeners) {\n listener(message);\n }\n }\n }\n }\n\n send(message: LspMessage): void {\n const content = JSON.stringify(message);\n const header = `Content-Length: ${Buffer.byteLength(content)}\\r\\n\\r\\n`;\n this.process.stdin?.write(header + content);\n }\n\n async request(\n method: string,\n params: unknown,\n timeoutMs = 10000,\n ): Promise<LspMessage> {\n const id = this.nextId++;\n\n return new Promise((resolve, reject) => {\n const timeout = setTimeout(() => {\n this.pendingRequests.delete(id);\n reject(\n new Error(`Timeout waiting for response to ${method} (id=${id})`),\n );\n }, timeoutMs);\n\n // Check if response already arrived\n const existing = this.messages.find((m) => m.id === id);\n if (existing) {\n clearTimeout(timeout);\n resolve(existing);\n return;\n }\n\n this.pendingRequests.set(id, {\n resolve: (msg) => {\n clearTimeout(timeout);\n resolve(msg);\n },\n reject: (err) => {\n clearTimeout(timeout);\n reject(err);\n },\n });\n\n this.send({ jsonrpc: '2.0', id, method, params });\n });\n }\n\n notify(method: string, params: unknown): void {\n this.send({ jsonrpc: '2.0', method, params });\n }\n\n openDocument(uri: string, languageId: string, text: string): void {\n this.notify('textDocument/didOpen', {\n textDocument: { uri, languageId, version: 1, text },\n });\n }\n\n changeDocument(uri: string, version: number, text: string): void {\n this.notify('textDocument/didChange', {\n textDocument: { uri, version },\n contentChanges: [{ text }],\n });\n }\n\n saveDocument(uri: string): void {\n this.notify('textDocument/didSave', {\n textDocument: { uri },\n });\n }\n\n waitForDiagnostics(uri: string, timeoutMs = 10000): Promise<LspDiagnostic[]> {\n return new Promise((resolve, reject) => {\n const deadline = setTimeout(() => {\n cleanup();\n reject(new Error(`Timeout waiting for diagnostics for ${uri}`));\n }, timeoutMs);\n\n const listener = (msg: LspMessage) => {\n if (msg.method !== 'textDocument/publishDiagnostics') return;\n const params = msg.params as {\n uri: string;\n diagnostics: LspDiagnostic[];\n };\n if (params.uri === uri) {\n cleanup();\n resolve(params.diagnostics);\n }\n };\n\n const cleanup = () => {\n clearTimeout(deadline);\n const idx = this.notificationListeners.indexOf(listener);\n if (idx !== -1) this.notificationListeners.splice(idx, 1);\n };\n\n // Check messages already received\n for (let i = this.messages.length - 1; i >= 0; i--) {\n const m = this.messages[i];\n if (m.method === 'textDocument/publishDiagnostics') {\n const params = m.params as {\n uri: string;\n diagnostics: LspDiagnostic[];\n };\n if (params.uri === uri) {\n clearTimeout(deadline);\n resolve(params.diagnostics);\n return;\n }\n }\n }\n\n this.notificationListeners.push(listener);\n });\n }\n\n /**\n * Waits for fresh diagnostics that arrive after this call.\n * Ignores any diagnostics already in the message buffer.\n */\n waitForFreshDiagnostics(\n uri: string,\n timeoutMs = 10000,\n ): Promise<LspDiagnostic[]> {\n return new Promise((resolve, reject) => {\n const deadline = setTimeout(() => {\n cleanup();\n reject(new Error(`Timeout waiting for fresh diagnostics for ${uri}`));\n }, timeoutMs);\n\n const listener = (msg: LspMessage) => {\n if (msg.method !== 'textDocument/publishDiagnostics') return;\n const params = msg.params as {\n uri: string;\n diagnostics: LspDiagnostic[];\n };\n if (params.uri === uri) {\n cleanup();\n resolve(params.diagnostics);\n }\n };\n\n const cleanup = () => {\n clearTimeout(deadline);\n const idx = this.notificationListeners.indexOf(listener);\n if (idx !== -1) this.notificationListeners.splice(idx, 1);\n };\n\n this.notificationListeners.push(listener);\n });\n }\n\n getMessages(): LspMessage[] {\n return [...this.messages];\n }\n\n getStderr(): string {\n return this.stderrChunks.join('');\n }\n\n getLogMessages(): string[] {\n return this.messages\n .filter((m) => m.method === 'window/logMessage')\n .map((m) => (m.params as { message: string }).message);\n }\n\n close(): void {\n this.process.kill();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst serverPath = path.join(__dirname, '..', 'dist', 'server.js');\n\n/**\n * Given file content and a search string, returns the 0-indexed\n * line/character position immediately after the last character of the match.\n */\nfunction cursorAfter(\n fileContent: string,\n searchText: string,\n): { line: number; character: number } {\n const idx = fileContent.indexOf(searchText);\n if (idx === -1) {\n throw new Error(`cursorAfter: \"${searchText}\" not found in content`);\n }\n const endIdx = idx + searchText.length;\n const before = fileContent.slice(0, endIdx);\n const lines = before.split('\\n');\n return {\n line: lines.length - 1,\n character: lines[lines.length - 1].length,\n };\n}\n\nasync function initializeClient(\n client: LspClient,\n rootUri: string,\n snippetSupport = true,\n): Promise<LspMessage> {\n const response = await client.request('initialize', {\n processId: null,\n rootUri,\n capabilities: {\n textDocument: {\n completion: {\n completionItem: {\n snippetSupport,\n },\n },\n synchronization: {\n didSave: true,\n },\n },\n },\n });\n client.notify('initialized', {});\n return response;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture: TypeScript project\n// ---------------------------------------------------------------------------\n\nconst TS_TEST_FILE_CONTENT = `import { sql } from '@514labs/moose-lib';\n\n// Valid SQL — should produce no diagnostics\nconst valid = sql\\`SELECT count() FROM users\\`;\n\n// Invalid SQL — should produce an error diagnostic\nconst invalid = sql\\`SELCT * FROM users\\`;\n\n// Completions: default context (cursor after \"SELECT \")\nconst comp1 = sql\\`SELECT \\`;\n\n// Context-aware: ENGINE =\nconst engine = sql\\`CREATE TABLE t ENGINE = \\`;\n\n// Context-aware: FORMAT\nconst format = sql\\`SELECT * FORMAT \\`;\n\n// Context-aware: SETTINGS\nconst settings = sql\\`SELECT * SETTINGS \\`;\n\n// Context-aware: column definition (data types)\nconst coldef = sql\\`CREATE TABLE t (id \\`;\n\n// Context-aware: FROM (table functions)\nconst fromCtx = sql\\`SELECT * FROM \\`;\n\n// Hover targets\nconst hover = sql\\`SELECT count() FROM users WHERE toUInt32(id) > 0\\`;\n\n// Format SQL target (lowercase)\nconst fmt = sql\\`select * from users where id = 1\\`;\n\n// Combinators\nconst comb = sql\\`SELECT sumIf(amount, active), countIf(status) FROM orders\\`;\n\n// Prefix filtering\nconst prefix = sql\\`SELECT cou\\`;\n\n// Combinator prefix filtering\nconst combPrefix = sql\\`SELECT sum\\`;\n\n// Default context (empty SQL — triggers Default completions with all kinds)\nconst defaultCtx = sql\\`\\`;\n`;\n\nasync function createTsFixture(): Promise<string> {\n const tmpDir = await fs.mkdtemp(\n path.join(os.tmpdir(), 'moose-lsp-integ-ts-'),\n );\n\n // package.json\n await fs.writeFile(\n path.join(tmpDir, 'package.json'),\n JSON.stringify({\n name: 'test-moose-ts',\n dependencies: { '@514labs/moose-lib': '*' },\n }),\n );\n\n // tsconfig.json\n await fs.writeFile(\n path.join(tmpDir, 'tsconfig.json'),\n JSON.stringify({\n compilerOptions: {\n target: 'ES2020',\n module: 'commonjs',\n strict: true,\n esModuleInterop: true,\n moduleResolution: 'node',\n },\n include: ['app/**/*.ts'],\n }),\n );\n\n // Minimal moose-lib shim\n const mooseLibDir = path.join(\n tmpDir,\n 'node_modules',\n '@514labs',\n 'moose-lib',\n );\n await fs.mkdir(mooseLibDir, { recursive: true });\n await fs.writeFile(\n path.join(mooseLibDir, 'package.json'),\n JSON.stringify({\n name: '@514labs/moose-lib',\n main: 'index.js',\n types: 'index.d.ts',\n }),\n );\n await fs.writeFile(\n path.join(mooseLibDir, 'index.js'),\n 'module.exports.sql = function sql() { return \"\"; };\\n',\n );\n await fs.writeFile(\n path.join(mooseLibDir, 'index.d.ts'),\n 'export declare function sql(strings: TemplateStringsArray, ...values: unknown[]): string;\\n',\n );\n\n // Test TypeScript file\n const appDir = path.join(tmpDir, 'app');\n await fs.mkdir(appDir, { recursive: true });\n await fs.writeFile(path.join(appDir, 'test.ts'), TS_TEST_FILE_CONTENT);\n\n return tmpDir;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture: Python project\n// ---------------------------------------------------------------------------\n\nconst PY_TEST_FILE_CONTENT = `from moose_lib import sql\n\n# Invalid SQL\nquery = sql(\"SELCT * FROM users\")\n`;\n\nasync function createPyFixture(): Promise<string> {\n const tmpDir = await fs.mkdtemp(\n path.join(os.tmpdir(), 'moose-lsp-integ-py-'),\n );\n\n await fs.writeFile(\n path.join(tmpDir, 'pyproject.toml'),\n `[project]\\nname = \"test-moose-py\"\\ndependencies = [\"moose-lib\"]\\n`,\n );\n\n const appDir = path.join(tmpDir, 'app');\n await fs.mkdir(appDir, { recursive: true });\n await fs.writeFile(path.join(appDir, 'test.py'), PY_TEST_FILE_CONTENT);\n\n return tmpDir;\n}\n\n// ---------------------------------------------------------------------------\n// Fixture: TypeScript project with docker-compose (version detection)\n// ---------------------------------------------------------------------------\n\nasync function createTsFixtureWithDockerCompose(\n chVersion: string,\n): Promise<string> {\n const tmpDir = await createTsFixture();\n\n await fs.writeFile(\n path.join(tmpDir, 'docker-compose.dev.override.yaml'),\n `services:\\n clickhousedb:\\n image: clickhouse/clickhouse-server:${chVersion}\\n`,\n );\n\n return tmpDir;\n}\n\n// ===========================================================================\n// TEST SUITES\n// ===========================================================================\n\n// ---------------------------------------------------------------------------\n// 1. TypeScript features (single server)\n// ---------------------------------------------------------------------------\n\ndescribe('TypeScript LSP features', () => {\n let client: LspClient;\n let tmpDir: string;\n const tsFileUri = () => `file://${path.join(tmpDir, 'app', 'test.ts')}`;\n\n before(async () => {\n tmpDir = await createTsFixture();\n client = new LspClient(serverPath);\n await initializeClient(client, `file://${tmpDir}`);\n // Open the test file — triggers initial validation\n client.openDocument(tsFileUri(), 'typescript', TS_TEST_FILE_CONTENT);\n // Wait for the initial diagnostics to arrive\n await client.waitForDiagnostics(tsFileUri());\n });\n\n after(async () => {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n });\n\n // ---- Feature 1: Error Diagnostics ----\n\n test('valid SQL produces no error diagnostics on that template', async () => {\n // The server publishes diagnostics for the whole file.\n // We check that the \"valid\" template line has no diagnostic.\n const validLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('const valid'),\n );\n const diagnostics = await client.waitForDiagnostics(tsFileUri());\n const onValidLine = diagnostics.filter(\n (d) => d.range.start.line === validLine,\n );\n assert.strictEqual(\n onValidLine.length,\n 0,\n `Expected no diagnostics on the valid SQL line, got: ${JSON.stringify(onValidLine)}`,\n );\n });\n\n test('invalid SQL produces an error diagnostic', async () => {\n const diagnostics = await client.waitForDiagnostics(tsFileUri());\n const invalidLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('const invalid'),\n );\n const onInvalidLine = diagnostics.filter(\n (d) => d.range.start.line === invalidLine,\n );\n assert.ok(\n onInvalidLine.length > 0,\n `Expected at least one diagnostic on the invalid SQL line (${invalidLine})`,\n );\n const diag = onInvalidLine[0];\n assert.strictEqual(diag.severity, 1, 'Severity should be Error (1)');\n assert.strictEqual(diag.source, 'moose-sql');\n });\n\n test('fixing SQL clears diagnostic on that line', async () => {\n const invalidLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('const invalid'),\n );\n\n const fixedContent = TS_TEST_FILE_CONTENT.replace(\n 'SELCT * FROM users',\n 'SELECT * FROM users',\n );\n\n // Send the change, wait for TextDocuments to process it, then save\n client.changeDocument(tsFileUri(), 2, fixedContent);\n await new Promise((r) => setTimeout(r, 100));\n client.saveDocument(tsFileUri());\n\n // Wait for diagnostics that no longer contain the SELCT error\n const deadline = Date.now() + 10000;\n let lastDiagnostics: LspDiagnostic[] = [];\n while (Date.now() < deadline) {\n lastDiagnostics = await client.waitForFreshDiagnostics(tsFileUri());\n const onFixedLine = lastDiagnostics.filter(\n (d) => d.range.start.line === invalidLine && d.source === 'moose-sql',\n );\n if (onFixedLine.length === 0) break;\n // Got stale diagnostics, wait for the next publish\n }\n\n const onFixedLine = lastDiagnostics.filter(\n (d) => d.range.start.line === invalidLine && d.source === 'moose-sql',\n );\n assert.strictEqual(\n onFixedLine.length,\n 0,\n `Expected no diagnostic on the fixed line (${invalidLine}), got: ${JSON.stringify(onFixedLine)}`,\n );\n\n // Restore original content for subsequent tests\n client.changeDocument(tsFileUri(), 3, TS_TEST_FILE_CONTENT);\n await new Promise((r) => setTimeout(r, 100));\n client.saveDocument(tsFileUri());\n await client.waitForFreshDiagnostics(tsFileUri());\n });\n\n // ---- Feature 2: Auto-Complete ----\n\n test('completions inside SQL template returns items', async () => {\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'comp1 = sql`SELECT ');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(Array.isArray(items), 'Result should be an array');\n assert.ok(items.length > 0, 'Should have completion items');\n });\n\n test('SelectClause context returns only functions', async () => {\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'comp1 = sql`SELECT ');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const kinds = new Set(items.map((i) => i.kind));\n assert.ok(\n kinds.has(CompletionItemKind.Function) ||\n kinds.has(CompletionItemKind.Method),\n 'Should include function or method completions',\n );\n // SelectClause context only returns functions (no keywords, data types, etc.)\n for (const item of items) {\n assert.ok(\n item.kind === CompletionItemKind.Function ||\n item.kind === CompletionItemKind.Method,\n `SelectClause should only have functions, got kind=${item.kind} for \"${item.label}\"`,\n );\n }\n });\n\n test('completions outside SQL template returns empty', async () => {\n // Line 0 is the import line — outside any sql template\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: { line: 0, character: 5 },\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(Array.isArray(items), 'Result should be an array');\n assert.strictEqual(\n items.length,\n 0,\n 'Should have no completions outside template',\n );\n });\n\n test('prefix filtering narrows completions', async () => {\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'prefix = sql`SELECT cou');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have some completions');\n const labels = items.map((i) => i.label.toLowerCase());\n assert.ok(\n labels.some((l) => l.startsWith('count')),\n `Should include count*, got: ${labels.slice(0, 10).join(', ')}`,\n );\n // Every item should start with 'cou'\n for (const item of items) {\n assert.ok(\n item.label.toLowerCase().startsWith('cou'),\n `Item \"${item.label}\" should start with \"cou\"`,\n );\n }\n });\n\n // ---- Feature 3: Context-Aware Completions ----\n\n test('ENGINE = context returns table engines', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'engine = sql`CREATE TABLE t ENGINE = ',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have engine completions');\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'MergeTree'),\n `Should include MergeTree, got: ${labels.slice(0, 10).join(', ')}`,\n );\n // All items should be engines (Class kind)\n for (const item of items) {\n assert.strictEqual(\n item.kind,\n CompletionItemKind.Class,\n `Engine item \"${item.label}\" should have kind=Class(${CompletionItemKind.Class}), got ${item.kind}`,\n );\n }\n });\n\n test('FORMAT context returns formats', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'format = sql`SELECT * FORMAT ',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have format completions');\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'JSON'),\n `Should include JSON, got: ${labels.slice(0, 10).join(', ')}`,\n );\n for (const item of items) {\n assert.strictEqual(\n item.kind,\n CompletionItemKind.Constant,\n `Format item \"${item.label}\" should have kind=Constant(${CompletionItemKind.Constant}), got ${item.kind}`,\n );\n }\n });\n\n test('SETTINGS context returns settings', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'settings = sql`SELECT * SETTINGS ',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have settings completions');\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'max_threads'),\n `Should include max_threads, got: ${labels.slice(0, 15).join(', ')}`,\n );\n for (const item of items) {\n assert.strictEqual(\n item.kind,\n CompletionItemKind.Property,\n `Setting item \"${item.label}\" should have kind=Property(${CompletionItemKind.Property}), got ${item.kind}`,\n );\n }\n });\n\n test('column definition context returns data types', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'coldef = sql`CREATE TABLE t (id ',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have data type completions');\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'UInt64'),\n `Should include UInt64, got: ${labels.slice(0, 15).join(', ')}`,\n );\n assert.ok(\n labels.some((l) => l === 'String'),\n `Should include String, got: ${labels.slice(0, 15).join(', ')}`,\n );\n });\n\n test('FROM context returns table functions', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'fromCtx = sql`SELECT * FROM ',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n assert.ok(items.length > 0, 'Should have FROM completions');\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'file' || l === 'url'),\n `Should include table functions like file/url, got: ${labels.slice(0, 15).join(', ')}`,\n );\n });\n\n // ---- Feature 4: Hover Documentation ----\n\n test('hover on known function shows documentation', async () => {\n // Find \"count\" in the hover line\n const hoverLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`SELECT count()'),\n );\n // \"count\" starts after \"SELECT \"\n const lineText = TS_TEST_FILE_CONTENT.split('\\n')[hoverLine];\n const countCharIdx = lineText.indexOf('count');\n assert.ok(countCharIdx !== -1, 'Should find count in hover line');\n\n const response = await client.request('textDocument/hover', {\n textDocument: { uri: tsFileUri() },\n position: { line: hoverLine, character: countCharIdx + 1 },\n });\n assert.ok(response.result, 'Hover should return a result for count');\n const hover = response.result as {\n contents: { kind: string; value: string };\n };\n assert.ok(\n hover.contents.value.length > 0,\n 'Hover content should not be empty',\n );\n assert.strictEqual(\n hover.contents.kind,\n 'markdown',\n 'Hover should be markdown',\n );\n });\n\n test('hover on keyword shows documentation', async () => {\n // Hover over SELECT on the hover line\n const hoverLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`SELECT count()'),\n );\n const lineText = TS_TEST_FILE_CONTENT.split('\\n')[hoverLine];\n const selectIdx = lineText.indexOf('SELECT');\n\n const response = await client.request('textDocument/hover', {\n textDocument: { uri: tsFileUri() },\n position: { line: hoverLine, character: selectIdx + 1 },\n });\n assert.ok(\n response.result,\n 'Hover should return a result for SELECT keyword',\n );\n });\n\n test('hover outside SQL template returns null', async () => {\n const response = await client.request('textDocument/hover', {\n textDocument: { uri: tsFileUri() },\n position: { line: 0, character: 5 },\n });\n assert.strictEqual(\n response.result,\n null,\n 'Hover outside template should be null',\n );\n });\n\n test('hover on unknown word returns null', async () => {\n // \"users\" is a table name, not a known ClickHouse function/keyword\n const hoverLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`SELECT count() FROM users'),\n );\n const lineText = TS_TEST_FILE_CONTENT.split('\\n')[hoverLine];\n const usersIdx = lineText.indexOf('users');\n\n const response = await client.request('textDocument/hover', {\n textDocument: { uri: tsFileUri() },\n position: { line: hoverLine, character: usersIdx + 1 },\n });\n assert.strictEqual(\n response.result,\n null,\n 'Hover on unknown word should be null',\n );\n });\n\n // ---- Feature 5: Code Actions — Format SQL ----\n\n test('Format SQL action is offered for valid lowercase SQL', async () => {\n const fmtLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`select * from users'),\n );\n\n const response = await client.request('textDocument/codeAction', {\n textDocument: { uri: tsFileUri() },\n range: {\n start: { line: fmtLine, character: 15 },\n end: { line: fmtLine, character: 15 },\n },\n context: { diagnostics: [] },\n });\n\n const actions = response.result as Array<{\n title: string;\n edit?: { changes: Record<string, Array<{ newText: string }>> };\n }>;\n assert.ok(Array.isArray(actions), 'Code actions should be an array');\n const formatAction = actions.find((a) => a.title.includes('Format'));\n assert.ok(formatAction, 'Should offer a Format SQL action');\n });\n\n test('Format SQL produces uppercase keywords', async () => {\n const fmtLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`select * from users'),\n );\n\n const response = await client.request('textDocument/codeAction', {\n textDocument: { uri: tsFileUri() },\n range: {\n start: { line: fmtLine, character: 15 },\n end: { line: fmtLine, character: 15 },\n },\n context: { diagnostics: [] },\n });\n\n const actions = response.result as Array<{\n title: string;\n edit?: { changes: Record<string, Array<{ newText: string }>> };\n }>;\n const formatAction = actions.find((a) => a.title.includes('Format'));\n assert.ok(formatAction?.edit, 'Format action should have an edit');\n const edits = Object.values(\n (\n formatAction as {\n edit: { changes: Record<string, Array<{ newText: string }>> };\n }\n ).edit.changes,\n ).flat();\n assert.ok(edits.length > 0, 'Should have at least one text edit');\n const newText = edits[0].newText;\n assert.ok(\n newText.includes('SELECT') && newText.includes('FROM'),\n `Formatted SQL should contain uppercase keywords, got: ${newText}`,\n );\n });\n\n test('Format SQL not offered for invalid SQL', async () => {\n const invalidLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('sql`SELCT * FROM users'),\n );\n\n const response = await client.request('textDocument/codeAction', {\n textDocument: { uri: tsFileUri() },\n range: {\n start: { line: invalidLine, character: 15 },\n end: { line: invalidLine, character: 15 },\n },\n context: { diagnostics: [] },\n });\n\n const actions = response.result as Array<{ title: string }>;\n const formatAction = (actions || []).find((a) =>\n a.title.includes('Format'),\n );\n assert.ok(!formatAction, 'Should NOT offer Format SQL for invalid SQL');\n });\n\n // ---- Feature 8: Aggregate Combinator Functions ----\n\n test('sumIf appears in completions after \"sum\" prefix', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'combPrefix = sql`SELECT sum',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'sumIf'),\n `Should include sumIf combinator, got: ${labels.slice(0, 20).join(', ')}`,\n );\n });\n\n test('combinator function has documentation', async () => {\n const pos = cursorAfter(\n TS_TEST_FILE_CONTENT,\n 'combPrefix = sql`SELECT sum',\n );\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const sumIf = items.find((i) => i.label === 'sumIf');\n assert.ok(sumIf, 'Should have sumIf item');\n assert.ok(sumIf.documentation, 'sumIf should have documentation');\n const docValue =\n typeof sumIf.documentation === 'string'\n ? sumIf.documentation\n : (sumIf.documentation as { value: string }).value;\n assert.ok(\n docValue.toLowerCase().includes('combinator') ||\n docValue.toLowerCase().includes('aggregate') ||\n docValue.toLowerCase().includes('if'),\n `sumIf docs should mention combinator/aggregate, got: ${docValue.slice(0, 200)}`,\n );\n });\n\n test('hover on combinator function shows info', async () => {\n const combLine = TS_TEST_FILE_CONTENT.split('\\n').findIndex((l) =>\n l.includes('countIf(status)'),\n );\n const lineText = TS_TEST_FILE_CONTENT.split('\\n')[combLine];\n const countIfIdx = lineText.indexOf('countIf');\n\n const response = await client.request('textDocument/hover', {\n textDocument: { uri: tsFileUri() },\n position: { line: combLine, character: countIfIdx + 1 },\n });\n assert.ok(response.result, 'Hover should return info for countIf');\n const hover = response.result as {\n contents: { kind: string; value: string };\n };\n assert.ok(\n hover.contents.value.length > 0,\n 'countIf hover should have content',\n );\n });\n\n // ---- Feature 9: Multi-word Keywords ----\n\n test('GROUP BY appears in default context completions', async () => {\n // Use the empty SQL template which triggers Default context (all completions)\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'defaultCtx = sql`');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'GROUP BY'),\n `Should include \"GROUP BY\" keyword, got sample: ${labels.slice(0, 30).join(', ')}`,\n );\n });\n\n test('ORDER BY appears in default context completions', async () => {\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'defaultCtx = sql`');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri: tsFileUri() },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const labels = items.map((i) => i.label);\n assert.ok(\n labels.some((l) => l === 'ORDER BY'),\n `Should include \"ORDER BY\" keyword, got sample: ${labels.slice(0, 30).join(', ')}`,\n );\n });\n});\n\n// ---------------------------------------------------------------------------\n// 2. Snippet toggle (separate servers needed)\n// ---------------------------------------------------------------------------\n\ndescribe('Snippet support toggle', () => {\n test('snippets enabled: function completion has snippet format', async () => {\n const tmpDir = await createTsFixture();\n const client = new LspClient(serverPath);\n\n try {\n await initializeClient(client, `file://${tmpDir}`, true);\n\n // Use the test.ts file already in the fixture (included in tsconfig)\n const uri = `file://${path.join(tmpDir, 'app', 'test.ts')}`;\n client.openDocument(uri, 'typescript', TS_TEST_FILE_CONTENT);\n await client.waitForDiagnostics(uri);\n\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'prefix = sql`SELECT cou');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const countItem = items.find((i) => i.label === 'count');\n assert.ok(\n countItem,\n `Should have count completion, got: ${items\n .map((i) => i.label)\n .slice(0, 10)\n .join(', ')}`,\n );\n assert.strictEqual(\n countItem.insertText,\n 'count($1)$0',\n 'Snippet mode should produce snippet insertText',\n );\n assert.strictEqual(\n countItem.insertTextFormat,\n 2,\n 'insertTextFormat should be Snippet (2)',\n );\n } finally {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n }\n });\n\n test('snippets disabled: function completion has plain format', async () => {\n const tmpDir = await createTsFixture();\n const client = new LspClient(serverPath);\n\n try {\n await initializeClient(client, `file://${tmpDir}`, false);\n\n // Use the test.ts file already in the fixture (included in tsconfig)\n const uri = `file://${path.join(tmpDir, 'app', 'test.ts')}`;\n client.openDocument(uri, 'typescript', TS_TEST_FILE_CONTENT);\n await client.waitForDiagnostics(uri);\n\n const pos = cursorAfter(TS_TEST_FILE_CONTENT, 'prefix = sql`SELECT cou');\n const response = await client.request('textDocument/completion', {\n textDocument: { uri },\n position: pos,\n });\n const items = response.result as LspCompletionItem[];\n const countItem = items.find((i) => i.label === 'count');\n assert.ok(\n countItem,\n `Should have count completion, got: ${items\n .map((i) => i.label)\n .slice(0, 10)\n .join(', ')}`,\n );\n assert.strictEqual(\n countItem.insertText,\n 'count()',\n 'Non-snippet mode should produce plain insertText',\n );\n assert.strictEqual(\n countItem.insertTextFormat,\n 1,\n 'insertTextFormat should be PlainText (1)',\n );\n } finally {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n }\n });\n});\n\n// ---------------------------------------------------------------------------\n// 3. ClickHouse version detection\n// ---------------------------------------------------------------------------\n\ndescribe('ClickHouse version detection', () => {\n test('detects version from docker-compose', async () => {\n const tmpDir = await createTsFixtureWithDockerCompose('25.6');\n const client = new LspClient(serverPath);\n\n try {\n await initializeClient(client, `file://${tmpDir}`);\n const logs = client.getLogMessages();\n assert.ok(\n logs.some((l) => l.includes('Detected ClickHouse version: 25.6')),\n `Should detect version 25.6. Logs: ${logs.join(' | ')}`,\n );\n } finally {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n }\n });\n\n test('falls back to latest when no docker-compose', async () => {\n const tmpDir = await createTsFixture();\n const client = new LspClient(serverPath);\n\n try {\n await initializeClient(client, `file://${tmpDir}`);\n const logs = client.getLogMessages();\n assert.ok(\n logs.some(\n (l) =>\n l.includes('No ClickHouse version detected, using latest') ||\n l.includes('using latest'),\n ),\n `Should fall back to latest. Logs: ${logs.join(' | ')}`,\n );\n } finally {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n }\n });\n});\n\n// ---------------------------------------------------------------------------\n// 4. Python project diagnostics\n// ---------------------------------------------------------------------------\n\ndescribe('Python LSP features', () => {\n test('Python invalid SQL produces a diagnostic', async () => {\n const tmpDir = await createPyFixture();\n const client = new LspClient(serverPath);\n\n try {\n await initializeClient(client, `file://${tmpDir}`);\n\n const pyUri = `file://${path.join(tmpDir, 'app', 'test.py')}`;\n client.openDocument(pyUri, 'python', PY_TEST_FILE_CONTENT);\n\n const diagnostics = await client.waitForDiagnostics(pyUri);\n assert.ok(\n diagnostics.length > 0,\n `Expected diagnostics for invalid Python SQL, got none`,\n );\n assert.strictEqual(\n diagnostics[0].severity,\n 1,\n 'Should be Error severity',\n );\n assert.strictEqual(diagnostics[0].source, 'moose-sql');\n } finally {\n client.close();\n await fs.rm(tmpDir, { recursive: true, force: true });\n }\n });\n});\n\n// ---------------------------------------------------------------------------\n// 5. Original basic tests (preserved)\n// ---------------------------------------------------------------------------\n\ndescribe('Server basic protocol', () => {\n test('responds to initialize request', async () => {\n const client = new LspClient(serverPath);\n\n try {\n const response = await client.request('initialize', {\n processId: null,\n rootUri: 'file:///tmp/test-project',\n capabilities: {},\n });\n\n assert.ok(response.result, 'Should have result');\n const result = response.result as {\n capabilities: { textDocumentSync: unknown };\n };\n assert.ok(result.capabilities, 'Should have capabilities');\n assert.ok(\n result.capabilities.textDocumentSync,\n 'Should have textDocumentSync capability',\n );\n } finally {\n client.close();\n }\n });\n\n test('handles didSave notification and logs message', async () => {\n const client = new LspClient(serverPath);\n\n try {\n await client.request('initialize', {\n processId: null,\n rootUri: 'file:///tmp/test-project',\n capabilities: {\n textDocument: {\n synchronization: {\n didSave: true,\n },\n },\n },\n });\n\n client.notify('initialized', {});\n await new Promise((resolve) => setTimeout(resolve, 100));\n\n client.openDocument(\n 'file:///tmp/test-project/app/test.ts',\n 'typescript',\n 'const x = 1;',\n );\n\n await new Promise((resolve) => setTimeout(resolve, 50));\n\n client.saveDocument('file:///tmp/test-project/app/test.ts');\n\n // Wait for the didSave log\n const deadline = Date.now() + 3000;\n let found = false;\n while (Date.now() < deadline) {\n const logs = client.getLogMessages();\n if (logs.some((l) => l.includes('didSave received'))) {\n found = true;\n break;\n }\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n\n assert.ok(found, 'Should log didSave received message');\n } finally {\n client.close();\n }\n });\n});\n"],"mappings":"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","names":["serverPath","before","assert","onFixedLine"]}
|