//! `wai-quantum lsp`: the engine's language service for editors, as a server //! of the language server protocol on standard input and output (each message //! a `Content-Length` header and its JSON). //! //! What a program means is the engine's to say: diagnostics are its reader's //! refusal and its critique's smells, hover shows its matrices and its //! counts, and the commands (Run, Plan, Critique, Shortcuts, Receipt) run //! here, through the engine the command line uses, a run signed with this //! user's key as `wai-quantum run` signs it. Nothing leaves the machine. //! //! The service itself is [`wai_quantum::engine::editor`]; this module only //! reads and writes the protocol, keeps the open documents, and turns byte //! offsets into the editor's positions (UTF-16 by default, UTF-8 when the //! editor offers it). use serde_json::{Value, json}; use std::collections::BTreeMap; use std::io::{BufRead, Write}; use std::sync::{Arc, Mutex}; use wai_quantum::ed25519_dalek::SigningKey; use wai_quantum::engine::editor::{self, Analysis, Severity, index::DeclKind}; use wai_quantum::engine::{catalog, critique, job, shortcuts}; use wai_quantum::qasm; /// What the commands run with: the memory a job may take, and the key a run is signed with. pub struct Config { pub memory_gb: u64, pub key: SigningKey, } /// The settings an editor may give, in `initializationOptions` or a /// configuration change, at the top level or under `wai-quantum`: the values a /// generator is read at, and the shots a run samples. #[derive(Clone, Debug, Default, PartialEq)] pub struct Settings { pub inputs: qasm::Inputs, pub shots: Option, } impl Settings { fn read(v: &Value) -> Option { let s = ["wai-quantum", "waiQuantum"].iter().find_map(|k| v.get(*k)).unwrap_or(v); if !s.is_object() { return None; } let inputs = s.get("inputs").and_then(Value::as_object).map(|o| o.iter().filter_map(|(k, v)| Some((k.clone(), v.as_i64()?))).collect()).unwrap_or_default(); Some(Settings { inputs, shots: s.get("shots").and_then(Value::as_u64) }) } } struct Doc { version: Value, analysis: Analysis, } type Out = Arc>>; struct Server { out: Out, docs: BTreeMap, utf8: bool, snippets: bool, settings: Settings, cfg: Arc, initialized: bool, shut_down: bool, workers: Vec>, } /// Writes one message. fn send(out: &Out, msg: &Value) { let body = msg.to_string(); if let Ok(mut w) = out.lock() { let _ = write!(w, "Content-Length: {}\r\n\r\n{body}", body.len()).and_then(|_| w.flush()); } } /// Reads one message: `Ok(None)` at the end of the input, `Err` for a body that is not JSON. fn read_message(r: &mut impl BufRead) -> std::io::Result>> { let mut len: Option = None; loop { let mut line = String::new(); if r.read_line(&mut line)? == 0 { return Ok(None); } let line = line.trim_end_matches(['\r', '\n']); if line.is_empty() { if len.is_some() { break; } continue; } if let Some((k, v)) = line.split_once(':') && k.trim().eq_ignore_ascii_case("content-length") { len = v.trim().parse().ok(); } } let mut buf = vec![0u8; len.unwrap_or(0)]; r.read_exact(&mut buf)?; Ok(Some(serde_json::from_slice(&buf).map_err(|e| e.to_string()))) } /// The protocol's error codes this server answers with. const METHOD_NOT_FOUND: i64 = -32601; const INVALID_PARAMS: i64 = -32602; const INTERNAL_ERROR: i64 = -32603; const NOT_INITIALIZED: i64 = -32002; const REQUEST_FAILED: i64 = -32803; /// Serves on standard input and output until the editor says `exit`; the /// process's exit status (0 after `shutdown`, 1 otherwise). pub fn serve(memory_gb: u64, key: SigningKey) -> i32 { let stdin = std::io::stdin(); serve_on(stdin.lock(), Box::new(std::io::stdout()), Config { memory_gb, key }) } /// [`serve`] on any reader and writer. pub fn serve_on(mut reader: impl BufRead, writer: Box, cfg: Config) -> i32 { let mut s = Server { out: Arc::new(Mutex::new(writer)), docs: BTreeMap::new(), utf8: false, snippets: false, settings: Settings::default(), cfg: Arc::new(cfg), initialized: false, shut_down: false, workers: Vec::new(), }; let code = loop { let msg = match read_message(&mut reader) { Ok(Some(Ok(m))) => m, Ok(Some(Err(e))) => { send(&s.out, &json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })); continue; } Ok(None) | Err(_) => break 1, }; let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string(); if method == "exit" { break if s.shut_down { 0 } else { 1 }; } let params = msg.get("params").cloned().unwrap_or(Value::Null); match msg.get("id").cloned() { Some(id) => { let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| s.request(&method, ¶ms, &id))); match reply { Ok(Some(Ok(result))) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "result": result })), Ok(Some(Err((code, message)))) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })), // A command answers from its own thread. Ok(None) => {} Err(_) => send(&s.out, &json!({ "jsonrpc": "2.0", "id": id, "error": { "code": INTERNAL_ERROR, "message": format!("{method} failed inside the server") } })), } } None => { let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| s.notification(&method, ¶ms))); } } }; for w in s.workers.drain(..) { let _ = w.join(); } code } type Reply = Option>; impl Server { fn capabilities(&self) -> Value { json!({ "positionEncoding": if self.utf8 { "utf-8" } else { "utf-16" }, "textDocumentSync": { "openClose": true, "change": 1, "save": false }, "completionProvider": { "resolveProvider": false }, "hoverProvider": true, "definitionProvider": true, "referencesProvider": true, "documentHighlightProvider": true, "renameProvider": { "prepareProvider": true }, "documentSymbolProvider": true, "foldingRangeProvider": true, "documentFormattingProvider": true, "codeLensProvider": { "resolveProvider": false }, "executeCommandProvider": { "commands": editor::COMMANDS.iter().map(|c| c.0).collect::>() }, }) } fn request(&mut self, method: &str, params: &Value, id: &Value) -> Reply { if method == "initialize" { let encodings = params.pointer("/capabilities/general/positionEncodings").and_then(Value::as_array).cloned().unwrap_or_default(); self.utf8 = encodings.iter().any(|e| e == "utf-8"); self.snippets = params.pointer("/capabilities/textDocument/completion/completionItem/snippetSupport") == Some(&Value::Bool(true)); if let Some(s) = params.get("initializationOptions").and_then(Settings::read) { self.settings = s; } self.initialized = true; return Some(Ok(json!({ "capabilities": self.capabilities(), "serverInfo": { "name": "wai-quantum", "version": env!("CARGO_PKG_VERSION") } }))); } if !self.initialized { return Some(Err((NOT_INITIALIZED, "the server has not been initialized".into()))); } if method == "shutdown" { self.shut_down = true; return Some(Ok(Value::Null)); } if method == "workspace/executeCommand" { return self.execute(params, id); } let uri = params.pointer("/textDocument/uri").and_then(Value::as_str).unwrap_or("").to_string(); let Some(doc) = self.docs.get(&uri) else { return Some(match method { m if m.starts_with("textDocument/") => Err((INVALID_PARAMS, format!("{uri} is not open"))), _ => Err((METHOD_NOT_FOUND, format!("no method {method}"))), }); }; let a = &doc.analysis; let at = || params.get("position").map(|p| self.offset(a, p)).unwrap_or(0); let loc = |s: editor::index::Span| json!({ "uri": uri, "range": self.range(a, s) }); Some(Ok(match method { "textDocument/completion" => { let items: Vec = editor::complete(a, at()).iter().map(|i| self.item(i)).collect(); json!({ "isIncomplete": false, "items": items }) } "textDocument/hover" => match editor::hover(a, at()) { Some(h) => json!({ "contents": { "kind": "markdown", "value": h.markdown }, "range": self.range(a, h.span) }), None => Value::Null, }, "textDocument/definition" => editor::definition(a, at()).map(loc).unwrap_or(Value::Null), "textDocument/references" => { let with_decl = params.pointer("/context/includeDeclaration") != Some(&Value::Bool(false)); Value::Array(editor::references(a, at(), with_decl).into_iter().map(loc).collect()) } "textDocument/documentHighlight" => { let spans = editor::references(a, at(), true); Value::Array(spans.iter().enumerate().map(|(i, &s)| json!({ "range": self.range(a, s), "kind": if i == 0 { 3 } else { 2 } })).collect()) } "textDocument/prepareRename" => match editor::prepare_rename(a, at()) { Ok((span, name)) => json!({ "range": self.range(a, span), "placeholder": name }), Err(e) => return Some(Err((REQUEST_FAILED, e))), }, "textDocument/rename" => { let new = params.get("newName").and_then(Value::as_str).unwrap_or(""); match editor::rename(a, at(), new) { Ok(spans) => { let edits: Vec = spans.iter().map(|&s| json!({ "range": self.range(a, s), "newText": new })).collect(); json!({ "changes": { uri.clone(): edits } }) } Err(e) => return Some(Err((REQUEST_FAILED, e))), } } "textDocument/documentSymbol" => Value::Array(editor::symbols(a).iter().map(|s| self.symbol(a, s)).collect()), "textDocument/foldingRange" => Value::Array( editor::folding(a) .iter() .map(|f| { let mut v = json!({ "startLine": f.start, "endLine": f.end }); if let Some(k) = f.kind { v["kind"] = json!(k); } v }) .collect(), ), "textDocument/formatting" => match editor::format(&a.src) { Ok(f) if f == a.src => json!([]), Ok(f) => json!([{ "range": self.range(a, (0, a.src.len())), "newText": f }]), Err(e) => return Some(Err((REQUEST_FAILED, e))), }, "textDocument/codeLens" => Value::Array( editor::lenses(a).iter().map(|l| json!({ "range": self.range(a, l.span), "command": { "title": l.title, "command": l.command, "arguments": [uri] } })).collect(), ), _ => return Some(Err((METHOD_NOT_FOUND, format!("no method {method}")))), })) } fn notification(&mut self, method: &str, params: &Value) { let uri = params.pointer("/textDocument/uri").and_then(Value::as_str).unwrap_or("").to_string(); match method { "textDocument/didOpen" => { let text = params.pointer("/textDocument/text").and_then(Value::as_str).unwrap_or(""); let version = params.pointer("/textDocument/version").cloned().unwrap_or(Value::Null); self.docs.insert(uri.clone(), Doc { version, analysis: Analysis::new(text) }); self.publish(&uri); } "textDocument/didChange" => { let Some(doc) = self.docs.get_mut(&uri) else { return }; let mut text = doc.analysis.src.clone(); for c in params.get("contentChanges").and_then(Value::as_array).into_iter().flatten() { let new = c.get("text").and_then(Value::as_str).unwrap_or(""); match c.get("range") { Some(r) => { let a = Analysis::new(&text); let (s, e) = (offset_in(&a, &r["start"], self.utf8), offset_in(&a, &r["end"], self.utf8)); text.replace_range(s.min(e)..e.max(s), new); } None => text = new.to_string(), } } doc.version = params.pointer("/textDocument/version").cloned().unwrap_or(Value::Null); doc.analysis = Analysis::new(&text); self.publish(&uri); } "textDocument/didClose" => { self.docs.remove(&uri); send(&self.out, &json!({ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "uri": uri, "diagnostics": [] } })); } "workspace/didChangeConfiguration" => { if let Some(s) = params.get("settings").and_then(Settings::read) { self.settings = s; let uris: Vec = self.docs.keys().cloned().collect(); for u in uris { self.publish(&u); } } } _ => {} } } fn publish(&self, uri: &str) { let Some(doc) = self.docs.get(uri) else { return }; let a = &doc.analysis; let diags: Vec = editor::diagnostics(a, &self.settings.inputs) .iter() .map(|d| { let severity = match d.severity { Severity::Error => 1, Severity::Warning => 2, Severity::Information => 3, Severity::Hint => 4, }; let mut v = json!({ "range": self.range(a, d.span), "severity": severity, "source": "wai-quantum", "message": d.message }); if let Some(c) = &d.code { v["code"] = json!(c); } v }) .collect(); send(&self.out, &json!({ "jsonrpc": "2.0", "method": "textDocument/publishDiagnostics", "params": { "uri": uri, "version": doc.version, "diagnostics": diags } })); } fn offset(&self, a: &Analysis, p: &Value) -> usize { offset_in(a, p, self.utf8) } fn position(&self, a: &Analysis, off: usize) -> Value { position_in(a, off, self.utf8) } fn range(&self, a: &Analysis, s: editor::index::Span) -> Value { json!({ "start": self.position(a, s.0), "end": self.position(a, s.1) }) } fn item(&self, i: &editor::Item) -> Value { use editor::ItemKind::*; let kind = match i.kind { Keyword => 14, Gate | Subroutine => 3, Register | Variable => 6, Constant => 21, Function => 3, Parameter => 25, Snippet => 15, }; let mut v = json!({ "label": i.label, "kind": kind, "detail": i.detail, "sortText": i.sort }); if let Some(d) = &i.doc { v["documentation"] = json!({ "kind": "markdown", "value": d }); } match (&i.snippet, &i.plain, self.snippets) { (Some(s), _, true) => { v["insertText"] = json!(s); v["insertTextFormat"] = json!(2); } (_, Some(p), _) => v["insertText"] = json!(p), _ => {} } v } fn symbol(&self, a: &Analysis, s: &editor::Symbol) -> Value { let kind = match s.kind { DeclKind::QubitRegister | DeclKind::BitRegister => 18, DeclKind::Gate | DeclKind::Subroutine | DeclKind::Extern => 12, DeclKind::Constant => 14, DeclKind::GateParameter | DeclKind::Argument => 26, _ => 13, }; json!({ "name": s.name, "detail": s.detail, "kind": kind, "range": self.range(a, s.span), "selectionRange": self.range(a, s.name_span), "children": s.children.iter().map(|c| self.symbol(a, c)).collect::>(), }) } /// A command: run on a thread of its own, so the editor's other requests /// are answered meanwhile; the answer is sent from there. fn execute(&mut self, params: &Value, id: &Value) -> Reply { let command = params.get("command").and_then(Value::as_str).unwrap_or("").to_string(); if !editor::COMMANDS.iter().any(|c| c.0 == command) { return Some(Err((INVALID_PARAMS, format!("no command {command}; the commands are {}", editor::COMMANDS.iter().map(|c| c.0).collect::>().join(", "))))); } let arg = params.pointer("/arguments/0").cloned().unwrap_or(Value::Null); let uri = arg.as_str().or_else(|| arg.get("uri").and_then(Value::as_str)).unwrap_or("").to_string(); let Some(doc) = self.docs.get(&uri) else { return Some(Err((INVALID_PARAMS, format!("{uri} is not open")))) }; let src = doc.analysis.src.clone(); let (settings, cfg, out, id) = (self.settings.clone(), self.cfg.clone(), self.out.clone(), id.clone()); let work = move || { let msg = match run_command(&command, &src, &settings, &cfg) { Ok(r) => { let level = if r["ok"] == true { 3 } else { 1 }; send(&out, &json!({ "jsonrpc": "2.0", "method": "window/logMessage", "params": { "type": level, "message": r["text"] } })); send(&out, &json!({ "jsonrpc": "2.0", "method": "window/showMessage", "params": { "type": level, "message": r["summary"] } })); json!({ "jsonrpc": "2.0", "id": id, "result": r }) } Err(e) => json!({ "jsonrpc": "2.0", "id": id, "error": { "code": REQUEST_FAILED, "message": e } }), }; send(&out, &msg); }; type Work = Box; let slot: Arc>> = Arc::new(Mutex::new(Some(Box::new(work)))); let theirs = slot.clone(); let spawned = std::thread::Builder::new().name("wai-quantum-command".into()).spawn(move || { if let Some(w) = theirs.lock().ok().and_then(|mut g| g.take()) { w() } }); match spawned { Ok(h) => self.workers.push(h), // Where there are no threads (a WASI build), the command runs here. Err(_) => { if let Some(w) = slot.lock().ok().and_then(|mut g| g.take()) { w() } } } self.workers.retain(|h| !h.is_finished()); None } } /// The byte offset of an editor's position. fn offset_in(a: &Analysis, p: &Value, utf8: bool) -> usize { let line = p.get("line").and_then(Value::as_u64).unwrap_or(0) as usize; let ch = p.get("character").and_then(Value::as_u64).unwrap_or(0) as usize; let Some(start) = a.lines.start(line) else { return a.src.len() }; let end = a.lines.start(line + 1).map(|e| e - 1).unwrap_or(a.src.len()); let text = &a.src[start..end]; let mut units = 0usize; for (i, c) in text.char_indices() { if units >= ch { return start + i; } units += if utf8 { c.len_utf8() } else { c.len_utf16() }; } end } /// An editor's position of a byte offset. fn position_in(a: &Analysis, off: usize, utf8: bool) -> Value { let off = off.min(a.src.len()); let line = a.line(off); let start = a.lines.start(line).unwrap_or(0); let text = &a.src[start..off]; let ch: usize = if utf8 { text.len() } else { text.chars().map(char::len_utf16).sum() }; json!({ "line": line, "character": ch }) } /// A command's answer: `ok`, a one-line `summary`, the `text` a reader takes /// in, and the engine's `json`. fn run_command(command: &str, src: &str, settings: &Settings, cfg: &Config) -> Result { let inputs = editor::read_inputs(src, &settings.inputs); let at = if inputs.is_empty() { String::new() } else { format!(" at {}", editor::inputs_text(&inputs)) }; // A generator runs as the circuit it writes out at its inputs. let program = || if inputs.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, &inputs) }; let request = |qasm: String| { let mut r = json!({ "qasm": qasm }); if let Some(s) = settings.shots { r["shots"] = json!(s); } r }; let answer = |ok: bool, summary: String, text: String, json: Value| json!({ "command": command, "ok": ok, "summary": summary, "text": text, "json": json }); match command { "wai-quantum.run" | "wai-quantum.receipt" => { let body = crate::run_here(&request(program()?), cfg.memory_gb, &cfg.key)?; let (verified, key) = job::verify(&body)?; let signer = key.as_str().and_then(job::did_of).unwrap_or_default(); let method = body["method"].as_str().unwrap_or("").to_string(); if command == "wai-quantum.run" { let summary = format!("Run{at}: {method}, {}; the receipt {}", body["result"]["accuracy"]["class"].as_str().unwrap_or(""), if verified { "verifies" } else { "DOES NOT VERIFY" }); return Ok(answer(verified, summary, job_text(&body, verified, &signer), body)); } let receipt = &body["result"]["receipt"]; let text = format!( "receipt {}\nsigned by {signer}\nbinds the request and the result, as given below\nmethod {method}\n\n{}\n", if verified { "verifies" } else { "DOES NOT VERIFY" }, serde_json::to_string_pretty(&body).unwrap_or_default() ); let summary = format!("Receipt{at}: {} by {signer}, digest {}", if verified { "verifies, signed" } else { "DOES NOT VERIFY, signed" }, receipt["digest"].as_str().unwrap_or("")); Ok(answer(verified, summary, text, body)) } "wai-quantum.plan" => { let p = catalog::plan(&request(program()?), cfg.memory_gb << 30)?; let text = format!("method {}\nwhy {}\nqubits {}\nmemory {:.1} MiB at most\nwork {}\n", p.method, p.why, p.qubits, p.memory_bytes as f64 / (1u64 << 20) as f64, p.work); let summary = format!("Plan{at}: {} — {}", p.method, p.why); Ok(answer(true, summary, text, serde_json::to_value(&p).unwrap_or(Value::Null))) } "wai-quantum.critique" => { let c = if inputs.is_empty() { critique::critique(src) } else { critique::critique_with(src, &inputs) }; if c["ok"] != true { return Err(c["error"].as_str().unwrap_or("the program does not read").to_string()); } let n = |k: &str| c[k].as_u64().unwrap_or(0); let summary = format!("Critique{at}: {} gates, {} two-qubit, depth {}, T count {}, {} smell{}", n("gates"), n("two_qubit"), n("depth"), n("t_count"), c["smells"].as_array().map_or(0, Vec::len), if c["smells"].as_array().map_or(0, Vec::len) == 1 { "" } else { "s" }); Ok(answer(true, summary, critique_text(&c), c)) } "wai-quantum.shortcuts" => { let v = shortcuts::shortcuts(&program()?, false); if v["ok"] != true { return Err(v["error"].as_str().unwrap_or("the program does not read").to_string()); } let summary = format!("Shortcuts{at}: {}", v["summary"].as_str().unwrap_or("")); Ok(answer(true, summary, shortcuts_text(&v), v)) } _ => Err(format!("no command {command}")), } } fn s(v: &Value) -> String { v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()) } /// A job as a reader takes it in. fn job_text(body: &Value, verified: bool, signer: &str) -> String { let r = &body["result"]; let mut out = format!("method {} — {}\n", s(&body["method"]), s(&body["why"])); if let Some(acc) = r.get("accuracy") { out.push_str(&format!("accuracy {}{}\n", s(&acc["class"]), acc.get("note").map(|n| format!(" ({})", s(n))).unwrap_or_default())); } out.push_str(&format!("circuit {} qubits, {} gates\n", r["qubits"], r["gates"])); if let Some(w) = r.get("warning") { out.push_str(&format!("warning {}\n", s(w))); } if let Some(counts) = r.get("counts").and_then(Value::as_object) { let total: u64 = counts.values().filter_map(Value::as_u64).sum::() + r["omitted_shots"].as_u64().unwrap_or(0); out.push_str(&format!("counts {total} shots, {} outcomes\n", r["distinct_outcomes"].as_u64().unwrap_or(counts.len() as u64))); let mut v: Vec<(&String, u64)> = counts.iter().map(|(k, c)| (k, c.as_u64().unwrap_or(0))).collect(); v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0))); for (k, c) in v.iter().take(16) { out.push_str(&format!(" {k} {c:>8} {:.4}\n", *c as f64 / total.max(1) as f64)); } if v.len() > 16 { out.push_str(&format!(" … {} more\n", v.len() - 16)); } } for e in r.get("expectations").and_then(Value::as_array).into_iter().flatten() { out.push_str(&format!("⟨{}⟩ {}\n", s(&e["observable"]), s(&e["value"]))); } out.push_str(&format!("receipt {}, signed by {signer}\n", if verified { "verifies" } else { "DOES NOT VERIFY" })); out } /// A critique as a reader takes it in. fn critique_text(c: &Value) -> String { let n = |k: &str| c[k].as_u64().unwrap_or(0); let mut out = format!( "{} qubits, {} gates ({} one-qubit, {} two-qubit, {} three-qubit), depth {}, two-qubit depth {}\n{} T gates; {}; {} qubits measured\n", n("qubits"), n("gates"), n("one_qubit"), n("two_qubit"), n("three_qubit"), n("depth"), n("two_qubit_depth"), n("t_count"), if c["clifford"] == true { "a Clifford circuit".to_string() } else { format!("{} gates off the Clifford angles", n("non_clifford")) }, n("measured") ); let f = &c["fidelity_model"]; out.push_str(&format!("fidelity {:.4}, modelled: {}\n", f["estimate"].as_f64().unwrap_or(f64::NAN), s(&f["note"]))); for sm in c["smells"].as_array().into_iter().flatten() { out.push_str(&format!("{}: {}{}\n", s(&sm["kind"]), s(&sm["message"]), if sm["fixable"] == true { " (improve fixes it)" } else { "" })); } out } /// The classical routes as a reader takes them in. fn shortcuts_text(v: &Value) -> String { let mut out = format!("{}\n\n", s(&v["summary"])); for r in v["routes"].as_array().into_iter().flatten() { let applies = r["applies"] == true; let cost = match (applies, r.get("log2_work").and_then(Value::as_f64)) { (true, Some(w)) => format!("2^{w:.2} operations"), (true, None) => "—".into(), _ => "no".into(), }; out.push_str(&format!("{:<15} {cost:<22} {}\n", s(&r["route"]), s(&r["why"]))); } out.push_str(&format!("\n{}\n", s(&v["note"]))); out } #[cfg(test)] mod tests { use super::*; #[test] fn positions_count_utf16_units_or_bytes_as_agreed() { let a = Analysis::new("rz(π) q;\nx q;\n"); // After `π`: four UTF-16 units in, five bytes. let at = a.src.find(')').unwrap(); assert_eq!(position_in(&a, at, false), json!({ "line": 0, "character": 4 })); assert_eq!(position_in(&a, at, true), json!({ "line": 0, "character": 5 })); assert_eq!(offset_in(&a, &json!({ "line": 0, "character": 4 }), false), at); assert_eq!(offset_in(&a, &json!({ "line": 0, "character": 5 }), true), at); assert_eq!(offset_in(&a, &json!({ "line": 1, "character": 0 }), false), a.src.find("x q").unwrap()); // Past a line's end: its end; past the last line: the end. assert_eq!(offset_in(&a, &json!({ "line": 1, "character": 99 }), false), a.src.len() - 1); assert_eq!(offset_in(&a, &json!({ "line": 9, "character": 0 }), false), a.src.len()); } #[test] fn a_message_is_read_by_its_length() { let body = r#"{"jsonrpc":"2.0","id":1,"method":"x"}"#; let text = format!("Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{body}Content-Length: 3\r\n\r\n{{x}}", body.len()); let mut r = std::io::Cursor::new(text.into_bytes()); assert_eq!(read_message(&mut r).unwrap().unwrap().unwrap()["id"], 1); assert!(read_message(&mut r).unwrap().unwrap().is_err()); assert!(read_message(&mut r).unwrap().is_none()); } #[test] fn settings_are_read_at_the_top_or_under_the_server_s_name() { let want = Settings { inputs: [("n".to_string(), 6)].into_iter().collect(), shots: Some(100) }; assert_eq!(Settings::read(&json!({ "inputs": { "n": 6 }, "shots": 100 })), Some(want.clone())); assert_eq!(Settings::read(&json!({ "wai-quantum": { "inputs": { "n": 6 }, "shots": 100 } })), Some(want)); assert_eq!(Settings::read(&json!(null)), None); } }