// 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 u8[4] values;\n return values[0];\n}\n'; assert.match(hover(array, '[4]'), /fixed length of 4/); assert.match(hover(array, 'values', 1), /\(u8\[4\]\) let values/); 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), /\(pair\) let value/); 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 +'), /\(int\) let left \/\/ field;[\s\S]*left field/); assert.match(hover(fields, 'right;'), /\(int\) let right \/\/ field;[\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 *'), /\(int\) let x \/\/ parameter;/); assert.match(hover(inferred, 'value;'), /\(int\) let value/); const pointer = 'int read(int* p) { return p[0]; }\nfn main() { int value = 3; return read(&value); }\n'; assert.match(hover(pointer, 'p[0]'), /\(int\*\) let p \/\/ parameter;[\s\S]*pointer to `int`/); const alias = 'type row = int[3];\nfn main() { row values; return values[0]; }\n'; assert.match(hover(alias, 'row values'), /int\[3\][\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;'), /\(color\) let shade/); const dynamic = 'fn main() { int[?] items; push(items, 3); return len(items); }\n'; assert.match(hover(dynamic, 'items);'), /\(int\[\?\]\) let items[\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)'), /\(.*\([\s\S]*callable value/); assert.match(hover(callable, 'apply(2)'), /\(int\(int\)\) let.m apply/); 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;'), /\(float\) let result/); const inferredPointer = 'include rt;\nstruct 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'), /\(node\*\) let.m p/); const aggregateAlias = 'fn main() { int[3] a; a[1] = 7; let alias = a; return alias[1]; }\n'; assert.match(hover(aggregateAlias, 'alias[1]'), /\(int\*\) let alias/); const sections = '// --- values ---\n// these declarations form a group.\nconst int limit = 7;\nfn main() { return limit; }'; assert.ok(!hover(sections, 'limit;').includes('these declarations form a group')); assert.ok(!hover(sections, 'limit;').includes('--- values ---')); const blockSection = '/* ---- values ---- */\nint limit = 7;\nfn main() { return limit; }'; assert.ok(!hover(blockSection, 'limit;').includes('---- values ----')); const trailing = 'fn main() {\n int n = 7; // the current count\n int next = 2;\n return n + next;\n}'; assert.match(hover(trailing, 'n +'), /the current count/); assert.ok(!hover(trailing, 'next;').includes('the current count')); const trailingBlock = 'fn main() { int a = 1, b = 2; /* final count */\n return a + b; }'; assert.ok(!hover(trailingBlock, 'a +').includes('final count')); assert.match(hover(trailingBlock, 'b;'), /final count/); const adjacent = 'fn main() { int a /* first count */ = 1, b /* second count */ = 2; return a + b; }'; assert.match(hover(adjacent, 'a +'), /first count/); assert.ok(!hover(adjacent, 'a +').includes('second count')); assert.match(hover(adjacent, 'b;'), /second count/); // section headings end a doc block; trailing comments belong to their own line's declaration const sectionDocs = '// ---- one ----\n\n// adds.\nint add(int a, int b) { return a + b; } // after add\n// overview\n// ---- two ----\nint sub(int a, int b) { return a - b; }\n// ---- three ----\n// own doc\nint mul(int a, int b) { return a * b; }\nint g = 3; /* after g */\nfn main() { return add(1, 2) + sub(3, 1) + mul(2, 2) + g; }\n'; const addHover = hover(sectionDocs, 'add(1'); assert.ok(addHover.includes('adds.') && addHover.includes('after add') && !addHover.includes('overview') && !addHover.includes('after g'), addHover); assert.strictEqual(hover(sectionDocs, 'sub(3'), '```zc\nint sub(int a, int b)\n```'); const mulHover = hover(sectionDocs, 'mul(2'); assert.ok(!mulHover.includes('own doc') && !mulHover.includes('three') && !mulHover.includes('after'), mulHover); assert.ok(hover(sectionDocs, 'g; }').includes('after g')); // ownership and transfers explained in hovers const lifeDocs = 'include rt; struct node { int v; node* next; };\nint*.u make(int n) { int* q = new(int); *q = n; return u*q; }\nfn take(.d p) { return p[0]; }\nfn main() {\n let a = make(1);\n node* b = new(node), c = new(node);\n b.next = c;\n node* r = b.next;\n let d = new(3);\n take(d*d);\n return a[0] + r.v;\n}\n'; assert.match(hover(lifeDocs, 'q = new'), /let.m q[\s\S]*passed up: returned on line 2/); assert.match(hover(lifeDocs, 'a ='), /let.m a[\s\S]*freed where its block ends on line 12/); assert.match(hover(lifeDocs, 'r.v'), /borrows from `b`, declared on line 6/); assert.match(hover(lifeDocs, 'c = new'), /passed down: stored in a container `b` on line 7/); assert.match(hover(lifeDocs, 'd = new'), /passed down: given to a d parameter on line 10/); const declaredResult = 'fn main() { let f = fn:float(int x) { return x; }; return f(2); }\n'; assert.match(hover(declaredResult, 'f(2)'), /\(float\(int\)\) let.m f/); const nonDocs = '//: scratch region overview\nint scratch = 1;\n/* public count */ int count = 2; //: implementation note\nfn main() { return scratch + count; }'; assert.ok(!hover(nonDocs, 'scratch +').includes('scratch region overview')); assert.match(hover(nonDocs, 'count;'), /public count/); assert.ok(!hover(nonDocs, 'count;').includes('implementation note')); const globals = 'let hello = "hello", prog[20000];\nint count = 3;\nfn main() { return hello[0] + prog[0] + count; }\n'; for (const [name, type, use] of [['hello', 'char*', 'hello[0]'], ['prog', 'word[20000]', 'prog[0]'], ['count', 'int', 'count;']]) { for (const needle of [name, use]) { const value = hover(globals, needle); assert.ok(value.startsWith('```zc\n(' + type + ') let ' + name + ' // global;\n```'), value); assert.ok(!value.includes('; type '), value); } } assert.match(hover('include run; fn main() { return run.sval[0]; }', 'sval'), /symbol[\s\S]*value/i); assert.match(hover('include rt; fn main() { return rt.len_s(b"bytes"); }', 'len_s'), /number of bytes in a packed byte string/i); console.log('ok built-in and composite types, inferred signatures, and separate block docs');