1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
// 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');
|