// focused type, inference, and block-documentation hover regressions. // usage: node tests/drivers/hover_features_test.js path/to/zclsp const assert = require('assert'); const cp = require('child_process'); const server = process.argv[2]; const uri = 'file:///hover-features.zc'; const frame = m => { const body = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]); }; function hover(text, needle, nth = 0) { let offset = -1; for (let i = 0; i <= nth; i++) offset = text.indexOf(needle, offset + 1); assert.ok(offset >= 0, 'fixture contains ' + needle); const before = text.slice(0, offset); const line = before.split('\n').length - 1; const character = before.length - before.lastIndexOf('\n') - 1; const messages = [ { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }, { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } }, { jsonrpc: '2.0', id: 2, method: 'textDocument/hover', params: { textDocument: { uri }, position: { line, character } } }, { jsonrpc: '2.0', method: 'exit' }, ]; const result = cp.spawnSync(server, process.argv.slice(3), { input: Buffer.concat(messages.map(frame)), timeout: 10000, maxBuffer: 4 << 20 }); assert.strictEqual(result.status, 0, String(result.stderr)); let data = result.stdout; while (data.length) { const end = data.indexOf('\r\n\r\n'); const count = Number(/Content-Length:\s*(\d+)/i.exec(data.subarray(0, end).toString())[1]); const msg = JSON.parse(data.subarray(end + 4, end + 4 + count).toString()); data = data.subarray(end + 4 + count); if (msg.id === 2) return msg.result && msg.result.contents.value; } return null; } for (const type of ['word', 'int', 'char', 'bool', 'float', 'f64', 'f32', 'byte', 'u8', 'i8', 'u16', 'i16', 'u32', 'i32']) { const value = hover(type + ' global;\nfn main() { return 0; }\n', type); assert.ok(value && value.startsWith('```zc\n') && value.includes('\n\n'), type + ' has formatted built-in documentation'); } assert.match(hover('void noop() {}\nfn main() { return 0; }\n', 'void'), /no meaningful result/); const array = 'fn main() {\n [4]u8 values;\n return values[0];\n}\n'; assert.match(hover(array, '[4]'), /fixed length of 4/); assert.match(hover(array, 'values', 1), /type \[4\]u8/); const struct = 'struct pair { int left; float right; };\nfn main() { pair value; return value.left; }\n'; assert.match(hover(struct, 'pair', 1), /int left;[\s\S]*float right;/); assert.match(hover(struct, 'value', 1), /type pair/); const fields = 'struct pair { int /* left field */ left, /* right field */ right; };\nfn main() { pair p; return p.left + p.right; }\n'; assert.match(hover(fields, 'left +'), /field; type int[\s\S]*left field/); assert.match(hover(fields, 'right;'), /field; type int[\s\S]*right field/); const empty = '/**/ int helper() { return 1; }\nfn main() { return helper(); }\n'; assert.match(hover(empty, 'helper();'), /int helper\(\)/); const variableDocs = 'fn main() {\n int /* first variable */ first = 1, /* second variable */ second = 2;\n return first + second;\n}\n'; const first = hover(variableDocs, 'first +'); const second = hover(variableDocs, 'second;'); assert.ok(first.includes('first variable') && !first.includes('second variable'), first); assert.ok(second.includes('second variable') && !second.includes('first variable'), second); const paramDocs = 'int add(int /* left argument */ a, int /* right argument */ b) { return a + b; }\nfn main() { return add(1, 2); }\n'; const a = hover(paramDocs, 'a +'); const b = hover(paramDocs, 'b;'); assert.ok(a.includes('left argument') && !a.includes('right argument'), a); assert.ok(b.includes('right argument') && !b.includes('left argument'), b); const functionDocs = '/**\n * adds **one** to its argument.\n */\nint add(int x) { return x + 1; }\nfn main() { return add(2); }\n'; assert.match(hover(functionDocs, 'add(2)'), /adds \*\*one\*\*/); const inferred = 'fn twice(x) { return x * 2; }\nfn main() { let value = twice(3); return value; }\n'; assert.match(hover(inferred, 'twice(3)'), /int twice\(int x\)/); assert.match(hover(inferred, 'x *'), /type int/); assert.match(hover(inferred, 'value;'), /type int/); const pointer = 'int read(int* p) { return p[0]; }\nfn main() { int value = 3; return read(&value); }\n'; assert.match(hover(pointer, 'p[0]'), /type int\*[\s\S]*pointer to `int`/); const alias = 'type row = [3]int;\nfn main() { row values; return values[0]; }\n'; assert.match(hover(alias, 'row values'), /\[3\]int[\s\S]*fixed length of 3/); const enumeration = 'enum color { red, green = 5, blue };\nfn main() { color shade = green; return shade; }\n'; assert.match(hover(enumeration, 'shade;'), /type color/); const dynamic = 'fn main() { []int items; push(items, 3); return len(items); }\n'; assert.match(hover(dynamic, 'items);'), /type \[\]int[\s\S]*growable array handle/); const callable = 'fn main() { let apply = fn(int x) { return x + 1; }; return apply(2); }\n'; assert.match(hover(callable, 'apply(2)'), /type .*\([\s\S]*callable value/); assert.match(hover(callable, 'apply(2)'), /type int\(int\)/); const floatClosure = 'fn main() { let f = fn(float x) { return x + 1.5; }; let result = f(2.0); return result; }\n'; assert.match(hover(floatClosure, 'result;'), /type float/); const inferredPointer = 'struct node { int value; };\nfn make_node() { return new(node); }\nfn main() { let p = make_node(); return p.value; }\n'; assert.match(hover(inferredPointer, 'p.value'), /type node\*/); const aggregateAlias = 'fn main() { [3]int a; a[1] = 7; let alias = a; return alias[1]; }\n'; assert.match(hover(aggregateAlias, 'alias[1]'), /type int\*/); console.log('ok built-in and composite types, inferred signatures, and separate block docs');