// 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 { convertCodeActions } = require('./codeactions'); const { vmProcess } = require('./vmproc'); const { wasmProcess } = require('./wasmproc'); const { DiagCache, diagKey, fileStamp, sha256 } = require('./diagcache'); const { hoverContents } = require('./hover'); const { createSourceProvider } = require('./sourceprovider'); 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 docFiles = new Map(); // uri -> the texts of the files the document includes, sent to the server before it checks the document const sentFiles = new Map(); // path -> the text the server was last sent for it 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', 'lsp.bin', '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); docFiles.set(uri, r.texts); 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'; // a borrow error names its owner: where it ended and where it was declared if (Array.isArray(d.relatedInformation)) { diag.relatedInformation = d.relatedInformation.filter(ri => ri && ri.location && ri.location.range).map(ri => { const lr = ri.location.range; return new vscode.DiagnosticRelatedInformation(new vscode.Location(vscode.Uri.parse(ri.location.uri || uri), new vscode.Range(lr.start.line, lr.start.character, lr.end.line, lr.end.character)), ri.message || ''); }); } 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; } // the server of this extension has no files of its own: it is sent the text of every file a document // includes, keyed by its full path, and looks an include up there. only what changed is sent again. function sendIncludes(uri) { const texts = docFiles.get(uri); if (!texts) return; for (const [p, text] of texts) { if (sentFiles.get(p) === text) continue; sentFiles.set(p, text); send({ jsonrpc: '2.0', method: 'zc/file', params: { path: p.split(path.sep).join('/'), text } }); } } // 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); sendIncludes(uri); 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(); sentFiles.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, rootUri: workspaceDir() ? vscode.Uri.file(workspaceDir()).toString() : null, 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)); // the sources the server carries (the runtime, compiler, standard library and icon module): shown read only at tinyvm:/NAME.zc // when go to definition lands on a built-in. the text comes from the server, so it is the code the editor's checks run on context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider('tinyvm', createSourceProvider(request, () => ready))); // completions refresh as the prefix changes; deprecated lifetime entries stay visible at the end. context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'zc' }, { async provideCompletionItems(doc, position, token) { if (!isZc(doc) || !ready || token.isCancellationRequested) return null; ensureServer(doc); const version = doc.version; try { const res = await request('textDocument/completion', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character } }); if (!res || token.isCancellationRequested || doc.version !== version) return null; const items = (res.items || []).map(c => { const item = new vscode.CompletionItem(c.label, c.kind ? c.kind - 1 : vscode.CompletionItemKind.Variable); item.detail = c.detail; item.sortText = c.sortText; item.filterText = c.filterText; if (c.tags && c.tags.includes(1)) item.tags = [vscode.CompletionItemTag.Deprecated]; if (c.documentation) { item.documentation = new vscode.MarkdownString(c.documentation.value || c.documentation); item.documentation.isTrusted = false; } if (c.textEdit) { const r = c.textEdit.range; item.range = new vscode.Range(r.start.line, r.start.character, r.end.line, r.end.character); item.insertText = c.textEdit.newText; } return item; }); return new vscode.CompletionList(items, !!res.isIncomplete); } catch (err) { return null; } } }, '.', '>')); // compiler-backed import fixes use the same document version as their diagnostics. if (vscode.languages.registerCodeActionsProvider) context.subscriptions.push(vscode.languages.registerCodeActionsProvider({ language: 'zc' }, { async provideCodeActions(doc, range, actionContext, token) { if (!isZc(doc) || !ready || token.isCancellationRequested) return []; ensureServer(doc); const version = doc.version; const serverVersion = docVersions.get(doc.uri.toString()); try { const results = await request('textDocument/codeAction', { textDocument: { uri: doc.uri.toString() }, range: { start: { line: range.start.line, character: range.start.character }, end: { line: range.end.line, character: range.end.character } }, context: { diagnostics: [] } }); if (token.isCancellationRequested || doc.version !== version || docVersions.get(doc.uri.toString()) !== serverVersion) return []; return convertCodeActions(vscode, results, doc, serverVersion); } catch (err) { return []; } } }, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] })); // go to definition and find references: the compiler's own records, including names from included files const locations = (res) => (Array.isArray(res) ? res : []).map(l => new vscode.Location(vscode.Uri.parse(l.uri), new vscode.Range(l.range.start.line, l.range.start.character, l.range.end.line, l.range.end.character))); context.subscriptions.push(vscode.languages.registerDefinitionProvider({ language: 'zc' }, { async provideDefinition(doc, position) { if (!isZc(doc) || !ready) return null; ensureServer(doc); try { return locations(await request('textDocument/definition', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character } })); } catch (err) { return null; } } })); // formatting: the server's pretty-printer, with the editor's tab settings (a .zcformat file overrides them) context.subscriptions.push(vscode.languages.registerDocumentFormattingEditProvider({ language: 'zc' }, { async provideDocumentFormattingEdits(doc, options) { if (!isZc(doc) || !ready) return []; ensureServer(doc); try { const res = await request('textDocument/formatting', { textDocument: { uri: doc.uri.toString() }, options: { tabSize: options.tabSize, insertSpaces: options.insertSpaces } }); return (Array.isArray(res) ? res : []).map(e => vscode.TextEdit.replace( new vscode.Range(e.range.start.line, e.range.start.character, e.range.end.line, e.range.end.character), e.newText)); } catch (err) { return []; } } })); context.subscriptions.push(vscode.languages.registerReferenceProvider({ language: 'zc' }, { async provideReferences(doc, position, refContext) { if (!isZc(doc) || !ready) return null; ensureServer(doc); try { return locations(await request('textDocument/references', { textDocument: { uri: doc.uri.toString() }, position: { line: position.line, character: position.character }, context: { includeDeclaration: !!refContext.includeDeclaration } })); } catch (err) { return null; } } })); } function deactivate() { log('message', 'extension deactivated: ' + JSON.stringify(stats)); stopServer(); } module.exports = { activate, deactivate };