aboutsummaryrefslogtreecommitdiff
path: root/editors/vscode/client/extension.js
diff options
context:
space:
mode:
authorGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
committerGravatar Moon Flower <moonflower@elhenney.local> 2026-10-12 08:46:23 +0330
commitecc350ca5c2eb50be8309c5f14f8bf0d87db9a02 (patch)
tree70e0af76a1b13affff7c6cc8aac7b3e6ef960478 /editors/vscode/client/extension.js
a
Diffstat (limited to 'editors/vscode/client/extension.js')
-rw-r--r--editors/vscode/client/extension.js537
1 files changed, 537 insertions, 0 deletions
diff --git a/editors/vscode/client/extension.js b/editors/vscode/client/extension.js
new file mode 100644
index 0000000..c51b12e
--- /dev/null
+++ b/editors/vscode/client/extension.js
@@ -0,0 +1,537 @@
+// vs code client for the zc language server. the server is the real compiler
+// running in check mode, so diagnostics, hints and highlighting follow what the
+// compiler does, including syntax a program adds to itself with parse words.
+//
+// everything the client does is written to the output channel "ZC Language
+// Server" (View > Output, then pick it from the list). zc.trace.server picks how
+// much: "off" logs only problems, "messages" (the default) logs each request and
+// response with its time, and "verbose" also logs the bodies. the command
+// "zc: show language server log" opens the channel and "zc: restart language
+// server" starts the server again.
+
+const vscode = require('vscode');
+const cp = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { vmProcess } = require('./vmproc');
+const { wasmProcess } = require('./wasmproc');
+const { DiagCache, diagKey, fileStamp, sha256 } = require('./diagcache');
+const { hoverContents } = require('./hover');
+
+const CHANNEL = 'ZC Language Server';
+const SLOW_MS = 2000; // a request that takes longer than this is logged as slow
+const INIT_WAIT_MS = 15000; // no answer to initialize by then is logged as a stall
+const CACHE_STATE = 'zc.diagnosticCache'; // the globalState entry the diagnostics cache is saved in
+
+let out = null;
+let trace = 'messages';
+let proc = null;
+let generation = 0; // each spawned server has its own; a stale exit is ignored
+let stopping = false; // set while the server is being shut down on purpose
+let ready = false;
+let diagnostics = null;
+let nextId = 1;
+let nextDocVersion = 1; // unique across close and reopen, even when the editor resets its version
+let restarts = 0;
+const pending = new Map();
+// the diagnostics cache (client/diagcache.js). the server is asked only when a
+// document's key is not in the cache, and the state below says what it holds
+let store = null; // the extension's globalState, where the cache is saved
+let cache = new DiagCache(null);
+let serverStamp = 'none'; // identifies the server build; it is part of every key
+const docKeys = new Map(); // uri -> key of the document as it is now (null: not cacheable)
+const docDeps = new Map(); // uri -> the files the document reads, for checking it again when one changes
+const serverKeys = new Map(); // uri -> key of the text the server holds
+const sentKeys = new Map(); // uri -> keys sent to the server whose diagnostics have not come back
+const docVersions = new Map(); // uri -> the last revision sent, including uncacheable documents
+const opened = new Set(); // uris the server has open
+let saveTimer = null;
+let initTimer = null;
+let inbox = Buffer.alloc(0);
+let stats = { requests: 0, tokens: 0, hovers: 0, diagnostics: 0, errors: 0, cacheHits: 0, cacheMisses: 0 };
+const changed = new vscode.EventEmitter();
+
+// the server's legend, in its order
+const types = ['keyword', 'type', 'function', 'variable', 'string', 'number', 'operator', 'comment', 'parameter', 'property', 'macro', 'enumMember'];
+const legend = new vscode.SemanticTokensLegend(types, []);
+
+function stamp() {
+ const d = new Date();
+ return d.toTimeString().slice(0, 8) + '.' + String(d.getMilliseconds()).padStart(3, '0');
+}
+
+// writes one line to the channel. level is 'problem' (always shown), 'message'
+// (the default) or 'verbose'
+function log(level, text) {
+ if (!out) return;
+ const rank = { off: 0, messages: 1, verbose: 2 };
+ const want = { problem: 0, message: 1, verbose: 2 }[level];
+ if (want > (rank[trace] ?? 1) && level !== 'problem') return;
+ out.appendLine(stamp() + ' ' + (level === 'problem' ? 'problem ' : '') + text);
+}
+
+function short(s, n) {
+ s = String(s);
+ return s.length > n ? s.slice(0, n) + '...(' + s.length + ' bytes)' : s;
+}
+
+// the native server when zc.lsp.serverPath names one; otherwise null, and the
+// server runs in the extension's own vm (server/ holds it and its image)
+function findServer() {
+ const configured = vscode.workspace.getConfiguration('zc').get('lsp.serverPath');
+ if (configured && fs.existsSync(configured)) return configured;
+ if (configured) {
+ log('problem', 'zc.lsp.serverPath does not exist: ' + configured + '; using the built-in server');
+ vscode.window.showWarningMessage('zc: zc.lsp.serverPath does not exist; running the server in the extension instead');
+ }
+ return null;
+}
+
+function send(msg) {
+ if (!proc || !proc.stdin.writable) {
+ log('problem', 'dropped ' + (msg.method || 'a reply') + ': no running server');
+ return;
+ }
+ const body = Buffer.from(JSON.stringify(msg), 'utf8');
+ if (msg.method) {
+ if (trace === 'verbose') log('verbose', '-> ' + msg.method + ' ' + short(JSON.stringify(msg.params), 300));
+ else log('message', '-> ' + msg.method + (msg.id !== undefined ? ' #' + msg.id : ''));
+ }
+ proc.stdin.write(Buffer.concat([Buffer.from('Content-Length: ' + body.length + '\r\n\r\n'), body]));
+}
+
+function request(method, params) {
+ return new Promise((resolve, reject) => {
+ if (!proc || !proc.stdin.writable) { reject({ message: 'server unavailable' }); return; }
+ const id = nextId++;
+ const t0 = Date.now();
+ stats.requests++;
+ pending.set(id, { resolve, reject, method, t0 });
+ send({ jsonrpc: '2.0', id, method, params });
+ });
+}
+
+function rejectPending(message) {
+ for (const p of pending.values()) p.reject({ message });
+ pending.clear();
+}
+
+// lengths are in bytes, so the buffer stays bytes until a whole message is in
+function onData(chunk) {
+ inbox = Buffer.concat([inbox, chunk]);
+ while (true) {
+ const end = inbox.indexOf('\r\n\r\n');
+ if (end < 0) return;
+ const m = /Content-Length:\s*(\d+)/i.exec(inbox.slice(0, end).toString('ascii'));
+ if (!m) {
+ log('problem', 'server sent bytes without a Content-Length header: ' + short(inbox.slice(0, end).toString('utf8'), 200));
+ inbox = inbox.slice(end + 4);
+ continue;
+ }
+ const start = end + 4, n = parseInt(m[1], 10);
+ if (inbox.length < start + n) return;
+ const body = inbox.slice(start, start + n).toString('utf8');
+ inbox = inbox.slice(start + n);
+ try {
+ onMessage(JSON.parse(body));
+ } catch (e) {
+ stats.errors++;
+ log('problem', 'bad message from server (' + e.message + '): ' + short(body, 300));
+ }
+ }
+}
+
+function onMessage(msg) {
+ if (msg.id !== undefined && pending.has(msg.id)) {
+ const p = pending.get(msg.id);
+ pending.delete(msg.id);
+ const ms = Date.now() - p.t0;
+ if (msg.error) {
+ stats.errors++;
+ log('problem', p.method + ' #' + msg.id + ' failed after ' + ms + ' ms: ' + short(JSON.stringify(msg.error), 300));
+ p.reject(msg.error);
+ return;
+ }
+ if (ms > SLOW_MS) log('problem', p.method + ' #' + msg.id + ' was slow: ' + ms + ' ms');
+ else log('message', '<- ' + p.method + ' #' + msg.id + ' in ' + ms + ' ms');
+ if (trace === 'verbose') log('verbose', '<- result ' + short(JSON.stringify(msg.result), 300));
+ p.resolve(msg.result);
+ return;
+ }
+ if (msg.method === 'textDocument/publishDiagnostics' && msg.params) {
+ onPublish(msg.params.uri, msg.params.diagnostics || [], msg.params.version);
+ return;
+ }
+ if (msg.method) log('message', '<- notification ' + msg.method + (msg.method.startsWith('window/') ? ' ' + short(JSON.stringify(msg.params), 200) : ''));
+}
+
+const isZc = doc => doc.languageId === 'zc';
+
+function workspaceDir() {
+ const f = vscode.workspace.workspaceFolders;
+ return f && f[0] ? f[0].uri.fsPath : null;
+}
+
+function shortKey(key) {
+ return key ? key.slice(0, 12) : 'none';
+}
+
+// the server build the cache keys are for: the native server's file, or the
+// built-in server's files
+function serverStampFor(bin) {
+ if (bin) return fileStamp(bin);
+ const dir = path.join(__dirname, '..', 'server');
+ return sha256(['lsp.wasm', 'lsp.json', 'tinyvm.js'].map(f => fileStamp(path.join(dir, f))).join(','));
+}
+
+// the key of a document as it is now, and what it reads (for checking it again when one of those changes)
+function keyOf(doc) {
+ const uri = doc.uri.toString();
+ const r = diagKey(serverStamp, doc.getText(), doc.uri.fsPath, workspaceDir());
+ docKeys.set(uri, r.key);
+ docDeps.set(uri, r.deps);
+ return r.key;
+}
+
+function showDiags(uri, raw) {
+ const list = raw.map(d => {
+ const r = d.range;
+ const sev = d.severity === 4 ? vscode.DiagnosticSeverity.Hint
+ : d.severity === 3 ? vscode.DiagnosticSeverity.Information
+ : d.severity === 2 ? vscode.DiagnosticSeverity.Warning
+ : vscode.DiagnosticSeverity.Error;
+ const diag = new vscode.Diagnostic(new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character), d.message, sev);
+ diag.source = d.source || 'zc';
+ return diag;
+ });
+ stats.diagnostics += list.length;
+ log('message', 'diagnostics for ' + uri + ': ' + list.length);
+ diagnostics.set(vscode.Uri.parse(uri), list);
+ changed.fire();
+}
+
+// the server's answer for a text it was sent. it is cached under the key that text had,
+// and shown only when that is still the document's key
+function onPublish(uri, raw, version) {
+ if (!opened.has(uri)) return;
+ const q = sentKeys.get(uri);
+ const index = q ? (version === undefined ? 0 : q.findIndex(entry => entry.version === version)) : -1;
+ if (!q || !q.length || index < 0) {
+ log('message', 'unrequested diagnostics for ' + uri + ': ignored');
+ return;
+ }
+ const { key, version: sentVersion } = q.splice(index, 1)[0];
+ if (key && cache.put(key, raw)) saveCacheSoon();
+ if (docKeys.get(uri) === key && (key || docVersions.get(uri) === sentVersion)) showDiags(uri, raw);
+ else log('message', 'diagnostics for ' + uri + ' under an old key: not shown');
+}
+
+// shows the cached diagnostics for doc when its key is in the cache; true when it did
+function showCached(doc) {
+ const uri = doc.uri.toString();
+ const key = keyOf(doc);
+ const hit = cache.get(key);
+ if (!hit) return false;
+ stats.cacheHits++;
+ log('message', 'diagnostics for ' + uri + ' from the cache (' + shortKey(key) + ')');
+ showDiags(uri, hit);
+ return true;
+}
+
+// sends doc's text to the server: didOpen the first time, didChange after that
+function sendDoc(doc, key) {
+ if (!proc || !proc.stdin.writable || !ready) return false;
+ const uri = doc.uri.toString();
+ const q = sentKeys.get(uri) || [];
+ const version = nextDocVersion++;
+ docVersions.set(uri, version);
+ q.push({ key, version });
+ sentKeys.set(uri, q);
+ serverKeys.set(uri, key);
+ if (!opened.has(uri)) {
+ opened.add(uri);
+ send({ jsonrpc: '2.0', method: 'textDocument/didOpen', params: { textDocument: { uri, languageId: 'zc', version, text: doc.getText() } } });
+ } else {
+ send({ jsonrpc: '2.0', method: 'textDocument/didChange', params: { textDocument: { uri, version }, contentChanges: [{ text: doc.getText() }] } });
+ }
+ return true;
+}
+
+// brings doc's diagnostics up to date. a key in the cache answers without the server, which
+// then only hears the text when something asks it about the document (see ensureServer).
+// otherwise the server gets the text and its answer is cached under the key
+function syncDoc(doc) {
+ if (!isZc(doc)) return;
+ const uri = doc.uri.toString();
+ if (showCached(doc)) return;
+ const key = docKeys.get(uri);
+ if (key !== null && key !== undefined && serverKeys.get(uri) === key && opened.has(uri)) return;
+ if (!ready) { log('message', 'not sending ' + uri + ' yet: server not ready'); return; }
+ stats.cacheMisses++;
+ sendDoc(doc, key === undefined ? keyOf(doc) : key);
+}
+
+// the server must hold doc's current text before a request about it (hover, tokens)
+function ensureServer(doc) {
+ if (!isZc(doc) || !ready) return false;
+ const uri = doc.uri.toString();
+ const key = keyOf(doc);
+ if (key !== null && serverKeys.get(uri) === key && opened.has(uri)) return true;
+ return sendDoc(doc, key);
+}
+
+// a file another open document reads changed on disk: that document is checked again
+function resyncDependents(fsPath) {
+ for (const doc of vscode.workspace.textDocuments) {
+ if (!isZc(doc) || doc.uri.fsPath === fsPath) continue;
+ const deps = docDeps.get(doc.uri.toString());
+ if (deps && deps.includes(fsPath)) {
+ log('message', 'included file changed: checking ' + doc.uri.toString() + ' again');
+ syncDoc(doc);
+ }
+ }
+}
+
+// the cache is written after a short pause, so typing does not write it on every key
+function saveCacheSoon() {
+ if (!store) return;
+ if (saveTimer) clearTimeout(saveTimer);
+ saveTimer = setTimeout(() => {
+ saveTimer = null;
+ try {
+ Promise.resolve(store.update(CACHE_STATE, cache.toJSON())).catch(e => log('problem', 'could not save the diagnostics cache: ' + e.message));
+ } catch (e) {
+ log('problem', 'could not save the diagnostics cache: ' + e.message);
+ }
+ }, 500);
+}
+
+// the server's answer to initialize is the first sign it works; without one the
+// log says so, and the usual cause is a server that is still loading
+// a native server that does not answer is stopped and the built-in server runs
+// instead. a built-in server that does not answer is only logged
+function watchInit(gen, native) {
+ if (initTimer) clearTimeout(initTimer);
+ initTimer = setTimeout(() => {
+ initTimer = null;
+ if (gen !== generation || ready || stopping || !proc) return;
+ if (native) {
+ log('problem', 'the native server at ' + zcServerPath() + ' did not answer initialize after ' + (INIT_WAIT_MS / 1000) + ' s; using the built-in server. clear zc.lsp.serverPath to stop seeing this');
+ forceBuiltin = true;
+ generation++;
+ if (proc) { try { proc.kill(); } catch (e) { /* already gone */ } }
+ proc = null;
+ rejectPending('server replaced after initialize timeout');
+ startServer();
+ } else {
+ log('problem', 'no answer to initialize after ' + (INIT_WAIT_MS / 1000) + ' s; the server is still starting or has stalled');
+ }
+ }, INIT_WAIT_MS);
+}
+
+function zcServerPath() {
+ return vscode.workspace.getConfiguration('zc').get('lsp.serverPath') || '';
+}
+
+// forceBuiltin is set when a native server did not answer: the built-in one runs instead
+let forceBuiltin = false;
+
+function startServer() {
+ const gen = ++generation;
+ stopping = false;
+ const bin = forceBuiltin ? null : findServer();
+ ready = false;
+ inbox = Buffer.alloc(0);
+ // a new server holds no documents and has answered nothing yet
+ opened.clear();
+ serverKeys.clear();
+ sentKeys.clear();
+ docVersions.clear();
+ serverStamp = serverStampFor(bin);
+ const t0 = Date.now();
+ try {
+ if (bin) {
+ log('message', 'starting the native server: ' + bin);
+ proc = cp.spawn(bin, [], { cwd: vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri.fsPath : undefined, stdio: ['pipe', 'pipe', 'pipe'] });
+ } else {
+ const dir = path.join(__dirname, '..', 'server');
+ const wasm = path.join(dir, 'lsp.wasm');
+ // the wasm server is the fast one; the javascript vm is kept for a build without it
+ if (fs.existsSync(wasm)) {
+ log('message', 'starting the built-in server (wasm) from ' + wasm);
+ proc = wasmProcess(wasm, { log: (level, text) => log(level, 'wasm: ' + text) });
+ } else {
+ log('message', 'starting the built-in server (javascript vm) from ' + dir);
+ proc = vmProcess(dir, { log: (level, text) => log(level, 'vm: ' + text) });
+ }
+ log('message', 'built-in server started in ' + (Date.now() - t0) + ' ms');
+ }
+ } catch (e) {
+ log('problem', 'could not start the server: ' + e.stack);
+ vscode.window.showErrorMessage('zc: could not start the language server: ' + e.message + ' (see the ZC Language Server output)');
+ return;
+ }
+ proc.stdout.on('data', data => { if (gen === generation) onData(data); });
+ proc.stderr.on('data', d => log('problem', 'server stderr: ' + short(d.toString(), 500)));
+ proc.on('error', err => {
+ if (gen !== generation) return;
+ ready = false;
+ rejectPending('server process error: ' + err.message);
+ log('problem', 'server process error: ' + err.stack);
+ vscode.window.showErrorMessage('zc: could not launch the language server: ' + err.message + ' (see the ZC Language Server output)');
+ });
+ proc.on('exit', (code, signal) => {
+ if (gen !== generation) return;
+ if (initTimer) { clearTimeout(initTimer); initTimer = null; }
+ if (stopping) { log('message', 'server stopped (code ' + code + ')'); ready = false; proc = null; rejectPending('server stopped'); return; }
+ log('problem', 'server exited with code ' + code + (signal ? ' signal ' + signal : '') + ' after ' + (Date.now() - t0) + ' ms');
+ ready = false;
+ proc = null;
+ rejectPending('server exited');
+ // a crash restarts the server, and the open documents are sent again once it is up
+ if (restarts < 5) {
+ restarts++;
+ log('problem', 'restarting the server (' + restarts + ' of 5)');
+ setTimeout(() => { if (gen === generation) startServer(); }, 300);
+ } else {
+ log('problem', 'giving up after 5 restarts; use "zc: restart language server" to try again');
+ }
+ });
+ watchInit(gen, !!bin);
+ request('initialize', { processId: process.pid, capabilities: {} }).then(res => {
+ if (gen !== generation || stopping || !proc) return;
+ if (initTimer) { clearTimeout(initTimer); initTimer = null; }
+ const caps = res && res.capabilities ? Object.keys(res.capabilities) : [];
+ log('message', 'server ready: capabilities ' + caps.join(', ') + ' (initialized in ' + (Date.now() - t0) + ' ms)');
+ send({ jsonrpc: '2.0', method: 'initialized', params: {} });
+ ready = true;
+ const docs = vscode.workspace.textDocuments;
+ log('message', 'syncing ' + docs.filter(isZc).length + ' open zc document(s)');
+ docs.forEach(syncDoc);
+ }).catch(err => log('problem', 'initialize failed: ' + short(JSON.stringify(err), 300)));
+}
+
+function stopServer() {
+ if (initTimer) { clearTimeout(initTimer); initTimer = null; }
+ if (!proc) { rejectPending('server stopped'); return; }
+ const p = proc;
+ log('message', 'stopping the server');
+ stopping = true;
+ ready = false;
+ rejectPending('server stopping');
+ request('shutdown', null).catch(() => {});
+ send({ jsonrpc: '2.0', method: 'exit' });
+ setTimeout(() => { if (proc === p) { proc.kill(); proc = null; ready = false; rejectPending('server stopped'); } }, 500);
+}
+
+function activate(context) {
+ out = vscode.window.createOutputChannel(CHANNEL);
+ context.subscriptions.push(out);
+ trace = vscode.workspace.getConfiguration('zc').get('trace.server') || 'messages';
+ log('message', 'extension activated; trace level ' + trace);
+
+ diagnostics = vscode.languages.createDiagnosticCollection('zc');
+ context.subscriptions.push(diagnostics);
+ store = context.globalState;
+ cache = new DiagCache(store ? store.get(CACHE_STATE) : null);
+ // the stamp is taken without findServer, which would warn about a missing server path a second time
+ const configured = vscode.workspace.getConfiguration('zc').get('lsp.serverPath');
+ serverStamp = serverStampFor(configured && fs.existsSync(configured) ? configured : null);
+ // the open documents whose diagnostics are cached show them before the server starts
+ vscode.workspace.textDocuments.forEach(d => { if (isZc(d)) showCached(d); });
+ startServer();
+
+ // a .zc file changed on disk (saved elsewhere, checked out, deleted): the open documents that include it are checked again
+ const watcher = vscode.workspace.createFileSystemWatcher('**/*.zc');
+ const recheck = uri => resyncDependents(uri.fsPath);
+ context.subscriptions.push(watcher, watcher.onDidChange(recheck), watcher.onDidCreate(recheck), watcher.onDidDelete(recheck));
+
+ context.subscriptions.push(
+ vscode.workspace.onDidChangeConfiguration(e => {
+ if (e.affectsConfiguration('zc.trace.server')) {
+ trace = vscode.workspace.getConfiguration('zc').get('trace.server') || 'messages';
+ log('message', 'trace level is now ' + trace);
+ }
+ }),
+ vscode.workspace.onDidOpenTextDocument(syncDoc),
+ vscode.workspace.onDidChangeTextDocument(e => syncDoc(e.document)),
+ vscode.workspace.onDidSaveTextDocument(doc => {
+ if (!isZc(doc)) return;
+ syncDoc(doc);
+ resyncDependents(doc.uri.fsPath);
+ }),
+ vscode.workspace.onDidCloseTextDocument(doc => {
+ if (!isZc(doc)) return;
+ const uri = doc.uri.toString();
+ diagnostics.delete(doc.uri);
+ docKeys.delete(uri);
+ docDeps.delete(uri);
+ serverKeys.delete(uri);
+ sentKeys.delete(uri);
+ docVersions.delete(uri);
+ if (opened.delete(uri) && ready) send({ jsonrpc: '2.0', method: 'textDocument/didClose', params: { textDocument: { uri } } });
+ }),
+ vscode.commands.registerCommand('zc.showLog', () => out.show(true)),
+ vscode.commands.registerCommand('zc.restart', () => {
+ log('message', 'restarting the server by request');
+ restarts = 0;
+ stopServer();
+ setTimeout(startServer, 600);
+ })
+ );
+
+ // highlighting: the server's tokens, on top of the grammar in syntaxes/
+ const tokens = {
+ onDidChangeSemanticTokens: changed.event,
+ async provideDocumentSemanticTokens(doc) {
+ if (!isZc(doc)) return null;
+ if (!ready) { log('message', 'no semantic tokens for ' + doc.uri.toString() + ': server not ready'); return null; }
+ const t0 = Date.now();
+ ensureServer(doc);
+ try {
+ const res = await request('textDocument/semanticTokens/full', { textDocument: { uri: doc.uri.toString() } });
+ if (!res || !res.data) { log('message', 'semantic tokens: none for ' + doc.uri.toString()); return null; }
+ const builder = new vscode.SemanticTokensBuilder(legend);
+ let line = 0, col = 0, count = 0;
+ for (let i = 0; i < res.data.length; i += 5) {
+ line += res.data[i];
+ col = res.data[i] === 0 ? col + res.data[i + 1] : res.data[i + 1];
+ builder.push(line, col, res.data[i + 2], res.data[i + 3], res.data[i + 4]);
+ count++;
+ }
+ stats.tokens += count;
+ log('message', 'semantic tokens: ' + count + ' for ' + doc.uri.toString() + ' in ' + (Date.now() - t0) + ' ms');
+ return builder.build();
+ } catch (err) {
+ log('problem', 'semantic tokens failed for ' + doc.uri.toString() + ': ' + short(JSON.stringify(err), 300));
+ return null;
+ }
+ }
+ };
+ context.subscriptions.push(vscode.languages.registerDocumentSemanticTokensProvider({ language: 'zc' }, tokens, legend));
+
+ // hover: the docs the compiler stores for a name (docs in the image)
+ const hover = {
+ async provideHover(doc, position) {
+ if (!isZc(doc) || !ready) return null;
+ ensureServer(doc);
+ try {
+ const res = await request('textDocument/hover', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character } });
+ stats.hovers++;
+ const contents = hoverContents(vscode, res && res.contents);
+ log('message', 'hover at ' + position.line + ':' + position.character + ': ' + (contents.length ? 'text' : 'nothing'));
+ return contents.length ? new vscode.Hover(contents) : null;
+ } catch (err) {
+ return null;
+ }
+ }
+ };
+ context.subscriptions.push(vscode.languages.registerHoverProvider({ language: 'zc' }, hover));
+}
+
+function deactivate() {
+ log('message', 'extension deactivated: ' + JSON.stringify(stats));
+ stopServer();
+}
+
+module.exports = { activate, deactivate };