From ee4900f847eec5a050c1cf909d5662efe82930f4 Mon Sep 17 00:00:00 2001 From: zinc Date: Thu, 15 Oct 2026 14:35:19 +0330 Subject: lsp: hold and shared hovers, completion of the keywords, shared variables marked in hover and completion; tmLanguage keywords; hold_lsp_test.js --- tests/drivers/hold_lsp_test.js | 54 ++++++++++++++++++++++++++++++++++++++++++ tests/test.sh | 3 +++ 2 files changed, 57 insertions(+) create mode 100644 tests/drivers/hold_lsp_test.js (limited to 'tests') diff --git a/tests/drivers/hold_lsp_test.js b/tests/drivers/hold_lsp_test.js new file mode 100644 index 0000000..65d8154 --- /dev/null +++ b/tests/drivers/hold_lsp_test.js @@ -0,0 +1,54 @@ +// hold blocks in the language server: hover on hold, shared and a shared variable, +// completion offers the keywords and marks shared variables, and a use outside the +// block is a diagnostic. usage: node tests/drivers/hold_lsp_test.js path/to/zclsp +const TIMEOUT_SCALE = Number(process.env.ZC_TEST_TIMEOUT_SCALE) || 1; +const assert = require('assert'); +const cp = require('child_process'); +const server = process.argv[2]; +const uri = 'file:///hold.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 session(text, request) { + const messages = [ + { jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }, + { jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, version: 1, languageId: 'zc', text } } }, + ]; + if (request) messages.push(Object.assign({ jsonrpc: '2.0', id: 2 }, request)); + messages.push({ jsonrpc: '2.0', method: 'exit' }); + const result = cp.spawnSync(server, [], { input: Buffer.concat(messages.map(frame)), timeout: 15000 * TIMEOUT_SCALE, maxBuffer: 8 << 20 }); + assert.strictEqual(result.status, 0, String(result.stderr)); + const out = []; 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]); + out.push(JSON.parse(data.subarray(end + 4, end + 4 + count).toString())); + data = data.subarray(end + 4 + count); + } + return out; +} +function at(text, needle, nth = 0, delta = 0) { + let o = -1; for (let i = 0; i <= nth; i++) o = text.indexOf(needle, o + 1); + assert.ok(o >= 0, needle); o += delta; + const before = text.slice(0, o); + return { line: before.split('\n').length - 1, character: before.length - before.lastIndexOf('\n') - 1 }; +} +const hover = (text, needle, nth) => { const r = session(text, { method: 'textDocument/hover', params: { textDocument: { uri }, position: at(text, needle, nth) } }).find(m => m.id === 2); return r && r.result && r.result.contents.value || ''; }; +const good = 'namespace;\ninclude rt;\nhold {\n shared let count = 0;\n fn inc() { count = count + 1; return count; }\n}\nfn main() { return inc(); }\n'; +let h = hover(good, 'hold'); +assert.ok(h.includes('monitor') && h.includes('lock'), 'hover on hold explains the block: ' + h); +h = hover(good, 'shared'); +assert.ok(h.includes('every actor'), 'hover on shared: ' + h); +h = hover(good, 'count', 1); +assert.ok(h.includes('shared in hold block 1'), 'hover on a shared variable names its block: ' + h); +const bad = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { count++; return count; } }\nfn main() { count = 2; return inc(); }\n'; +const diags = session(bad).filter(m => m.method === 'textDocument/publishDiagnostics').flatMap(m => m.params.diagnostics); +assert.ok(diags.some(d => d.message.includes('count: is shared in a hold block') && d.range.start.line === 3), 'use outside the block is a diagnostic on its line: ' + JSON.stringify(diags)); +const partial = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { count++; return count; } }\nho\nfn main() { return inc(); }\n'; +let r = session(partial, { method: 'textDocument/completion', params: { textDocument: { uri }, position: at(partial, '\nho', 0, 3) } }).find(m => m.id === 2); +let items = (r.result.items || r.result); +assert.ok(items.some(i => i.label === 'hold'), 'completion offers hold'); +const partial2 = 'namespace;\ninclude rt;\nhold { shared let count; fn inc() { cou; return count; } }\nfn main() { return inc(); }\n'; +r = session(partial2, { method: 'textDocument/completion', params: { textDocument: { uri }, position: at(partial2, 'cou;', 0, 3) } }).find(m => m.id === 2); +items = (r.result.items || r.result); +const it = items.find(i => i.label === 'count'); +assert.ok(it && String(it.detail).startsWith('shared'), 'completion marks a shared variable: ' + JSON.stringify(it)); +console.log('hold lsp: 6 checks passed'); diff --git a/tests/test.sh b/tests/test.sh index 3b505fc..90599ed 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -646,6 +646,9 @@ s_lsp() { node tests/drivers/codeactions_client_test.js > $T/codeactions-client.out 2>&1; r=$? grep -v '^ok' $T/codeactions-client.out chk $r 0 "lsp: versioned import quick-fix edits" + node tests/drivers/hold_lsp_test.js $lsp > $T/holdlsp.out 2>&1; r=$? + cat $T/holdlsp.out | grep -v passed + chk $r 0 "lsp hold blocks: hover, completion and diagnostics" node tests/drivers/completion_test.js $lsp > $T/completion.out 2>&1; r=$? grep -v '^ok' $T/completion.out chk $r 0 "lsp: fuzzy completions, signatures, scope and expired lifetimes" -- cgit v1.2.3