// compiler-backed completion, including incomplete source and position-aware lifetimes. // usage: node tests/drivers/completion_test.js path/to/zclsp [server arguments] // interpreted hosts (no jit) are far slower; ZC_TEST_TIMEOUT_SCALE stretches every deadline const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; const assert = require('assert'); const cp = require('child_process'); const path = require('path'); const uri = 'file://' + path.resolve('tests/drivers/completion-fixture.zc'); const frame = m => { const b = Buffer.from(JSON.stringify(m)); return Buffer.concat([Buffer.from(`Content-Length: ${b.length}\r\n\r\n`), b]); }; function complete(source, extra = []) { const at = source.indexOf('|'); assert.ok(at >= 0); const before = source.slice(0, at), text = source.replace('|', ''); const position = { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 }; const msgs = [ { id: 1, method: 'initialize', params: {} }, { method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, text } } }, { id: 2, method: 'textDocument/completion', params: { textDocument: { uri }, position } }, ...extra.map(m => ({ ...m, params: { textDocument: { uri }, position, ...m.params } })), { method: 'exit' } ].map(m => ({ jsonrpc: '2.0', ...m })); let output; if (process.argv[2] === '--wasm' || process.argv[2] === '--vm') { const wasm = process.argv[2] === '--wasm'; const host = require(path.resolve('editors/vscode/client', wasm ? 'wasmproc.js' : 'vmproc.js')); const proc = (wasm ? host.wasmProcess : host.vmProcess)(path.resolve(process.argv[3]), { log: () => {} }); const chunks = []; proc.stdout.on('data', b => chunks.push(b)); for (const m of msgs) proc.stdin.write(frame(m)); output = Buffer.concat(chunks); } else { const r = cp.spawnSync(process.argv[2], process.argv.slice(3), { input: Buffer.concat(msgs.map(frame)), timeout: 30000 * TIMEOUT_SCALE, maxBuffer: 32 << 20 }); assert.strictEqual(r.status, 0, String(r.stderr)); output = r.stdout; } let b = output; const replies = []; while (b.length) { const e = b.indexOf('\r\n\r\n'); assert.ok(e >= 0, b.toString()); const n = Number(/Content-Length:\s*(\d+)/i.exec(b.subarray(0, e).toString())[1]); replies.push(JSON.parse(b.subarray(e + 4, e + 4 + n))); b = b.subarray(e + 4 + n); } assert.ok(replies.find(m => m.id === 1).result.capabilities.completionProvider); const reply = replies.find(m => m.id === 2); assert.ok(reply && reply.result, JSON.stringify(reply)); return { items: reply.result.items, replies }; } const get = (items, label) => { const item = items.find(c => c.label === label); assert.ok(item, `${label} missing: ${items.map(c => c.label)}`); return item; }; let r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() {\n int tally = 3;\n ta|\n}\n'); assert.match(get(r.items, 'tally').detail, /int tally/); assert.ok(!r.items.some(c => c.label === 'count'), 'other function parameters are out of scope'); r = complete('/* computes a total */\nint total(int count) { return count + 1; }\nfn main() { to| }'); assert.match(get(r.items, 'total').detail, /int total\(int count\)/); assert.match(get(r.items, 'total').documentation.value, /computes a total/); r = complete('fn main() {\n int count = 1;\n { int hidden = 3; }\n |\n}\n'); get(r.items, 'count'); assert.ok(!r.items.some(c => c.label === 'hidden')); r = complete('include rt; fn main(int input) {\n let p = rt.new(int, 3);\n rt.free(p);\n |\n}\n'); const p = get(r.items, 'p'); assert.deepStrictEqual(p.tags, [1]); assert.match(p.documentation.value, /lifetime ended/); get(r.items, 'input'); assert.ok(r.items.findIndex(c => c.label === 'p') > r.items.findIndex(c => c.label === 'input')); assert.ok(r.items.slice(r.items.findIndex(c => c.tags)).every(c => c.tags), 'all consumed variables sort last'); r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n p|;\n rt.free(p);\n return 0;\n}'); assert.ok(!get(r.items, 'p').tags, 'later consumption does not expire earlier completion'); r = complete('fn main() {\n int old = 1;\n int bold = 2;\n int other_local_data = 3;\n old|\n}'); assert.strictEqual(r.items[0].label, 'old', 'exact match comes first'); get(r.items, 'other_local_data'); r = complete('include std;\nfn main() { std.Text.at| }'); assert.match(get(r.items, 'atoi').detail, /atoi\(/); assert.ok(r.items.every(c => !c.label.includes('.')), 'member labels omit receiver'); r = complete('struct point { int x; int y; };\nfn main() { point p; p.| }'); get(r.items, 'x'); get(r.items, 'y'); assert.ok(!r.items.some(c => c.label === 'main')); r = complete('fn main() { Te| }'); assert.ok(!r.items.some(c => c.label === 'Text'), 'std is not imported automatically'); r = complete('int shared;\nfn main() { char shared = 1; sh| }'); assert.match(get(r.items, 'shared').detail, /char shared/); assert.strictEqual(r.items.filter(c => c.label === 'shared').length, 1); r = complete('fn main() { let hello = "hi"; hel| }'); assert.match(get(r.items, 'hello').detail, /char\* hello/); r = complete('fn main() { let bytes = b"hi"; byt| }'); assert.match(get(r.items, 'bytes').detail, /bstr bytes/); r = complete('fn main() { int count = 1; cou|nt; return count; }', [{ id: 3, method: 'textDocument/hover' }]); assert.ok(r.replies.find(m => m.id === 3).result, 'hover still works after a partial completion check'); assert.strictEqual(get(r.items, 'count').textEdit.newText, 'count'); assert.strictEqual(get(r.items, 'count').textEdit.range.end.character - get(r.items, 'count').textEdit.range.start.character, 5); r = complete('include rt; fn main() {\n let p = rt.new(int, 3);\n let alias = p;\n rt.free(p);\n |\n}'); assert.deepStrictEqual(get(r.items, 'alias').tags, [1], 'freed aliases are also unavailable'); r = complete('include rt; fn take(.d p) { rt.free(p); }\nfn main() {\n let p = rt.new(int, 3);\n take(d*p);\n |\n}'); assert.deepStrictEqual(get(r.items, 'p').tags, [1], 'ownership transfers are unavailable'); r = complete('fn main() {\n int outer = 1;\n let f = fn(int inner) {\n ou|\n };\n}'); get(r.items, 'outer'); r = complete('fn first() { int wrong = 1; return wrong; }\n|\nfn second() { int other = 2; return other; }'); assert.ok(!r.items.some(c => c.label === 'wrong' || c.label === 'other'), 'top-level completion excludes function locals'); r = complete('fn main() {\n /* the latest count */\n int latest = 1;\n la|'); assert.match(get(r.items, 'latest').documentation.value, /the latest count/); r = complete('include rt; fn main() { int[?] items = {}; items.| }'); for (const name of ['len', 'cap', 'push', 'pop', 'resize']) get(r.items, name); assert.match(get(r.items, 'push').detail, /push\(int value\)/); assert.match(get(r.items, 'pop').detail, /int pop\(\)/); assert.match(get(r.items, 'resize').documentation.value, /length/); assert.ok(!r.items.some(c => c.label === 'main'), 'array member completion excludes globals'); r = complete('fn main() { int[3] items = {1,2,3}; items.| }'); get(r.items, 'len'); get(r.items, 'cap'); assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed arrays omit mutation methods'); r = complete('include rt; fn main() { fixed int[?] items = rt.new(fixed int[?], 3); items.| }'); get(r.items, 'len'); get(r.items, 'cap'); assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'fixed handles omit mutation methods'); r = complete('include std; fn main() { int[3] items = {1,2,3}; items.| }'); for (const name of ['find', 'contains', 'find_if', 'any', 'all', 'count', 'map', 'filter', 'reduce']) get(r.items, name); assert.match(get(r.items, 'map').documentation.value, /transform.*each element/); assert.ok(!r.items.some(c => ['push', 'pop', 'resize'].includes(c.label)), 'library algorithms do not make a fixed array growable'); r = complete('include rt; fn main() { int[?] items = {}; items.| }'); assert.ok(!r.items.some(c => ['map', 'filter'].includes(c.label)), 'library methods require an explicit import'); r = complete('include std; fn main() { std.| }'); for (const name of ['File', 'Io', 'Text', 'Host', 'argv', 'bnew']) get(r.items, name); assert.ok(!r.items.some(c => c.label.includes('.')), 'module completion lists one level: ' + r.items.filter(c => c.label.includes('.')).map(c => c.label)); r = complete('include std; fn main() { std.File.| }'); for (const name of ['read', 'write']) get(r.items, name); assert.ok(!r.items.some(c => ['Io', 'File', 'main', 'int'].includes(c.label)), 'group completion lists only its members'); r = complete('include std; fn main() { std.Io.| }'); for (const name of ['print', 'println', 'put_n', 'put_f', 'getline']) get(r.items, name); assert.ok(!r.items.some(c => ['write', 'itoa', 'main', 'int'].includes(c.label)), 'std.Io. lists only Io members'); r = complete('include std; fn main() { std.Io.pr| }'); assert.deepStrictEqual(r.items.map(c => c.label).sort(), ['print', 'println']); console.log('ok completion: signatures, docs, fuzzy ranking, scope, array members, shadowing, consumed lifetimes and one level of a module path');