1use std::collections::{HashMap, VecDeque};
15use std::io::{BufRead, BufReader, Write};
16use std::path::{Path, PathBuf};
17use std::process::{Child, Command, Stdio};
18use std::sync::mpsc::{self, Receiver};
19use std::time::{Duration, Instant};
20
21use anyhow::{Context, Result};
22use serde_json::Value;
23
24pub const PINNED_PYRIGHT: &str = "1.1.411";
26pub const SERVER_CMD: &str = "pyright-langserver";
28pub const SERVER_ARGS: &[&str] = &["--stdio"];
30pub const ENV_CMD_OVERRIDE: &str = "PITE_LSP_CMD";
32pub const STUBS_FILE: &str = "pite.pyi";
34const INIT_TIMEOUT: Duration = Duration::from_secs(20);
36
37pub fn missing_server_message(cmd: &str) -> String {
39 format!(
40 "LSP unavailable: `{cmd}` not found on PATH. Install the pinned server \
41 (`pip install \"pyright=={PINNED_PYRIGHT}\"` ships `{SERVER_CMD}`) or point \
42 `{ENV_CMD_OVERRIDE}` at one. Editing continues without completion, hover, \
43 signature help, or server diagnostics."
44 )
45}
46
47pub fn path_to_uri(path: &Path) -> String {
50 let abs = if path.is_absolute() {
51 path.to_path_buf()
52 } else {
53 std::env::current_dir()
54 .unwrap_or_else(|_| PathBuf::from("."))
55 .join(path)
56 };
57 uri_from_path_text(&abs.to_string_lossy())
58}
59
60pub fn uri_from_path_text(path: &str) -> String {
66 let normalized = path.replace('\\', "/");
67 let mut out = String::with_capacity(normalized.len() + 8);
68 out.push_str("file://");
69 if !normalized.starts_with('/') {
70 out.push('/');
71 }
72 for c in normalized.chars() {
73 match c {
74 ' ' => out.push_str("%20"),
75 '#' => out.push_str("%23"),
76 '?' => out.push_str("%3F"),
77 '%' => out.push_str("%25"),
78 other => out.push(other),
79 }
80 }
81 out
82}
83
84pub fn byte_offset_of_char(text: &str, char_offset: usize) -> usize {
86 text.char_indices()
87 .map(|(b, _)| b)
88 .nth(char_offset)
89 .unwrap_or(text.len())
90}
91
92pub fn offset_to_position(text: &str, byte_offset: usize) -> (u32, u32) {
95 let at = byte_offset.min(text.len());
96 let mut line = 0u32;
97 let mut units = 0u32;
98 for (b, c) in text.char_indices() {
99 if b >= at {
100 break;
101 }
102 if c == '\n' {
103 line += 1;
104 units = 0;
105 } else {
106 units += c.len_utf16() as u32;
107 }
108 }
109 (line, units)
110}
111
112pub fn line_start_byte(text: &str, line: usize) -> usize {
114 if line <= 1 {
115 return 0;
116 }
117 let mut seen = 1usize;
118 for (b, c) in text.char_indices() {
119 if c == '\n' {
120 seen += 1;
121 if seen >= line {
122 return b + 1;
123 }
124 }
125 }
126 text.len()
127}
128
129pub fn word_start_before(text: &str, byte_offset: usize) -> usize {
132 let mut start = byte_offset.min(text.len());
133 while start > 0 {
134 match text[..start].chars().next_back() {
135 Some(c) if c.is_alphanumeric() || c == '_' => start -= c.len_utf8(),
136 _ => break,
137 }
138 }
139 start
140}
141
142pub fn word_end_after(text: &str, byte_offset: usize) -> usize {
145 let mut end = byte_offset.min(text.len());
146 for c in text[end..].chars() {
147 if c.is_alphanumeric() || c == '_' {
148 end += c.len_utf8();
149 } else {
150 break;
151 }
152 }
153 end
154}
155
156pub fn parse_version(text: &str) -> Option<(u64, u64, u64)> {
158 let mut digits = String::new();
159 for c in text.chars() {
160 if c.is_ascii_digit() || c == '.' {
161 digits.push(c);
162 } else if !digits.is_empty() {
163 break;
164 }
165 }
166 let mut parts = digits.split('.');
167 Some((
168 parts.next()?.parse().ok()?,
169 parts.next()?.parse().ok()?,
170 parts.next()?.parse().ok()?,
171 ))
172}
173
174pub fn version_at_least(have: (u64, u64, u64), want: (u64, u64, u64)) -> bool {
176 have >= want
177}
178
179fn pin_triple() -> (u64, u64, u64) {
180 let mut parts = PINNED_PYRIGHT.split('.');
181 (
182 parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
183 parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
184 parts.next().and_then(|p| p.parse().ok()).unwrap_or(0),
185 )
186}
187
188fn version_output(cmd: &str) -> Option<String> {
189 let out = Command::new(cmd).arg("--version").output().ok()?;
190 if !out.status.success() {
191 return None;
192 }
193 Some(String::from_utf8_lossy(&out.stdout).into_owned())
194}
195
196fn sibling_version(cmd: &str) -> Option<String> {
198 if Path::new(cmd).file_name().and_then(|s| s.to_str()) != Some(SERVER_CMD) {
199 return None;
200 }
201 let sibling = match Path::new(cmd).parent() {
202 Some(parent) if !parent.as_os_str().is_empty() => {
203 parent.join("pyright").to_string_lossy().into_owned()
204 }
205 _ => "pyright".to_string(),
206 };
207 version_output(&sibling)
208}
209
210pub fn probe_server_version(cmd: &str) -> (Option<String>, Option<String>) {
213 let text = version_output(cmd).or_else(|| sibling_version(cmd));
214 let Some(text) = text else {
215 return (
216 None,
217 Some(format!(
218 "LSP: `{cmd} --version` failed; proceeding without a version check."
219 )),
220 );
221 };
222 match parse_version(&text) {
223 Some(have) => {
224 let label = format!("{}.{}.{}", have.0, have.1, have.2);
225 if version_at_least(have, pin_triple()) {
226 (Some(label), None)
227 } else {
228 (
229 Some(label.clone()),
230 Some(format!(
231 "LSP: server is {label} but {PINNED_PYRIGHT} is pinned; \
232 consider upgrading (`pip install \"pyright=={PINNED_PYRIGHT}\"`)."
233 )),
234 )
235 }
236 }
237 None => (
238 None,
239 Some(format!(
240 "LSP: could not parse server version from {text:?}; proceeding."
241 )),
242 ),
243 }
244}
245
246pub fn find_stubs_dir(start: &Path) -> Option<PathBuf> {
250 let mut dir = if start.is_file() {
251 start.parent()?.to_path_buf()
252 } else {
253 start.to_path_buf()
254 };
255 loop {
256 let nested = dir.join("python").join("pite").join(STUBS_FILE);
257 if nested.is_file() {
258 return Some(dir.join("python").join("pite"));
259 }
260 if dir.join(STUBS_FILE).is_file() {
261 return Some(dir);
262 }
263 if !dir.pop() {
264 return None;
265 }
266 }
267}
268
269const EMBEDDED_STUBS: &str = include_str!("../python/pite.pyi");
275const EMBEDDED_SOURCE: &str = include_str!("../python/pite.py");
276
277pub fn write_stubs_to(dir: &Path) -> Result<PathBuf> {
280 for (name, contents) in [(STUBS_FILE, EMBEDDED_STUBS), ("pite.py", EMBEDDED_SOURCE)] {
281 let path = dir.join(name);
282 let current = std::fs::read_to_string(&path).unwrap_or_default();
283 if current != contents {
284 if let Some(parent) = path.parent() {
285 std::fs::create_dir_all(parent)?;
286 }
287 std::fs::write(&path, contents)?;
288 }
289 }
290 Ok(dir.to_path_buf())
291}
292
293pub fn materialized_stubs_dir() -> Option<PathBuf> {
296 let dir = std::env::temp_dir().join(format!("pite-{}", env!("CARGO_PKG_VERSION")));
297 write_stubs_to(&dir).ok()
298}
299
300pub fn encode_message(body: &Value) -> Vec<u8> {
302 let text = serde_json::to_string(body).expect("LSP body serializes");
303 let mut out = format!("Content-Length: {}\r\n\r\n", text.len()).into_bytes();
304 out.extend_from_slice(text.as_bytes());
305 out
306}
307
308pub struct FramingReader<R: BufRead> {
310 inner: R,
311}
312
313impl<R: BufRead> FramingReader<R> {
314 pub fn new(inner: R) -> Self {
315 Self { inner }
316 }
317
318 pub fn read_message(&mut self) -> Result<Value> {
319 let mut len: Option<usize> = None;
320 loop {
321 let mut line = String::new();
322 let n = self
323 .inner
324 .read_line(&mut line)
325 .context("server closed stdio")?;
326 if n == 0 {
327 anyhow::bail!("server closed stdio");
328 }
329 let line = line.trim();
330 if line.is_empty() {
331 break;
332 }
333 if let Some(rest) = line.strip_prefix("Content-Length:") {
334 len = Some(rest.trim().parse().context("bad Content-Length")?);
335 }
336 }
337 let len = len.context("message without Content-Length")?;
338 let mut buf = vec![0u8; len];
339 self.inner
340 .read_exact(&mut buf)
341 .context("server closed stdio")?;
342 serde_json::from_slice(&buf).context("server sent invalid JSON")
343 }
344}
345
346fn request(id: i64, method: &str, params: Value) -> Value {
347 serde_json::json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params})
348}
349
350fn notification(method: &str, params: Value) -> Value {
351 serde_json::json!({"jsonrpc": "2.0", "method": method, "params": params})
352}
353
354pub fn initialize_request(id: i64, root_uri: Option<&str>) -> Value {
362 let folders: Value = root_uri
363 .map(|uri| serde_json::json!([{"uri": uri, "name": "pite"}]))
364 .unwrap_or_else(|| serde_json::json!([]));
365 request(
366 id,
367 "initialize",
368 serde_json::json!({
369 "processId": std::process::id(),
370 "rootUri": root_uri,
371 "workspaceFolders": folders,
372 "capabilities": {
373 "workspace": {"configuration": true},
374 "textDocument": {"publishDiagnostics": {}},
375 },
376 }),
377 )
378}
379
380pub fn initialized_notification() -> Value {
381 notification("initialized", serde_json::json!({}))
382}
383
384pub fn did_change_configuration(stubs_dir: Option<&Path>) -> Value {
386 let extra: Vec<String> = stubs_dir
387 .map(|d| vec![d.to_string_lossy().into_owned()])
388 .unwrap_or_default();
389 notification(
390 "workspace/didChangeConfiguration",
391 serde_json::json!({"settings": {
392 "python": {"analysis": {
393 "extraPaths": extra,
394 "typeCheckingMode": "basic",
395 }},
396 }}),
397 )
398}
399
400pub fn configuration_response(id: i64, section: &str, stubs_dir: Option<&Path>) -> Value {
407 let extra: Vec<String> = stubs_dir
408 .map(|d| vec![d.to_string_lossy().into_owned()])
409 .unwrap_or_default();
410 let result = if section == "python" {
411 serde_json::json!({"analysis": {
412 "extraPaths": extra,
413 "typeCheckingMode": "basic",
414 }})
415 } else {
416 serde_json::json!({})
417 };
418 serde_json::json!({"jsonrpc": "2.0", "id": id, "result": [result]})
419}
420
421pub fn did_open_notification(uri: &str, text: &str) -> Value {
422 notification(
423 "textDocument/didOpen",
424 serde_json::json!({"textDocument": {
425 "uri": uri, "languageId": "python", "version": 1, "text": text,
426 }}),
427 )
428}
429
430pub fn did_change_notification(uri: &str, version: i32, text: &str) -> Value {
431 notification(
432 "textDocument/didChange",
433 serde_json::json!({"textDocument": {"uri": uri, "version": version},
434 "contentChanges": [{"text": text}]}),
435 )
436}
437
438fn position_params(uri: &str, line: u32, character: u32) -> Value {
439 serde_json::json!({"textDocument": {"uri": uri},
440 "position": {"line": line, "character": character}})
441}
442
443pub fn completion_request(id: i64, uri: &str, line: u32, character: u32) -> Value {
444 request(
445 id,
446 "textDocument/completion",
447 position_params(uri, line, character),
448 )
449}
450
451pub fn hover_request(id: i64, uri: &str, line: u32, character: u32) -> Value {
452 request(
453 id,
454 "textDocument/hover",
455 position_params(uri, line, character),
456 )
457}
458
459pub fn signature_request(id: i64, uri: &str, line: u32, character: u32) -> Value {
460 request(
461 id,
462 "textDocument/signatureHelp",
463 position_params(uri, line, character),
464 )
465}
466
467#[derive(Debug, Clone, PartialEq)]
469pub struct Diagnostic {
470 pub line: u32,
471 pub character: u32,
472 pub severity: u32,
473 pub message: String,
474}
475
476#[derive(Debug, Clone, PartialEq)]
478pub struct CompletionItem {
479 pub label: String,
480 pub detail: String,
481}
482
483pub fn parse_publish_diagnostics(params: &Value) -> Vec<Diagnostic> {
485 let Some(items) = params.get("diagnostics").and_then(Value::as_array) else {
486 return Vec::new();
487 };
488 items
489 .iter()
490 .map(|d| Diagnostic {
491 line: d
492 .pointer("/range/start/line")
493 .and_then(Value::as_u64)
494 .unwrap_or(0) as u32,
495 character: d
496 .pointer("/range/start/character")
497 .and_then(Value::as_u64)
498 .unwrap_or(0) as u32,
499 severity: d.get("severity").and_then(Value::as_u64).unwrap_or(1) as u32,
500 message: d
501 .get("message")
502 .and_then(Value::as_str)
503 .unwrap_or("")
504 .to_string(),
505 })
506 .collect()
507}
508
509pub fn parse_completion_items(result: &Value) -> Vec<CompletionItem> {
511 let items = match result {
512 Value::Array(items) => items.clone(),
513 Value::Object(_) => result
514 .get("items")
515 .and_then(Value::as_array)
516 .cloned()
517 .unwrap_or_default(),
518 _ => Vec::new(),
519 };
520 items
521 .iter()
522 .map(|i| CompletionItem {
523 label: i
524 .get("label")
525 .and_then(Value::as_str)
526 .unwrap_or("")
527 .to_string(),
528 detail: i
529 .get("detail")
530 .and_then(Value::as_str)
531 .unwrap_or("")
532 .to_string(),
533 })
534 .filter(|i| !i.label.is_empty())
535 .collect()
536}
537
538pub fn parse_hover(result: &Value) -> String {
540 let contents = match result {
541 Value::Null => return String::new(),
542 Value::Object(_) => result.get("contents").unwrap_or(&Value::Null),
543 other => other,
544 };
545 match contents {
546 Value::String(s) => s.clone(),
547 Value::Array(parts) => parts
548 .iter()
549 .filter_map(|p| {
550 p.get("value")
551 .and_then(Value::as_str)
552 .map(str::to_string)
553 .or_else(|| p.as_str().map(str::to_string))
554 })
555 .collect::<Vec<_>>()
556 .join("\n"),
557 Value::Object(_) => contents
558 .get("value")
559 .and_then(Value::as_str)
560 .unwrap_or("")
561 .to_string(),
562 _ => String::new(),
563 }
564}
565
566pub fn parse_signature_help(result: &Value) -> Vec<String> {
568 result
569 .get("signatures")
570 .and_then(Value::as_array)
571 .map(|sigs| {
572 sigs.iter()
573 .filter_map(|s| s.get("label").and_then(Value::as_str).map(str::to_string))
574 .collect()
575 })
576 .unwrap_or_default()
577}
578
579#[derive(Debug, Clone, Copy, PartialEq)]
580enum PendingKind {
581 Initialize,
582 Completion,
583 Hover,
584 Signature,
585}
586
587pub trait RpcTransport: Send {
590 fn send(&mut self, body: &Value) -> Result<()>;
591 fn drain(&mut self) -> Vec<Value>;
592 fn alive(&mut self) -> bool;
593}
594
595pub struct StdioTransport {
598 stdin: std::process::ChildStdin,
599 inbox: Receiver<Value>,
600 dead: bool,
601}
602
603impl StdioTransport {
604 pub fn spawn(cmd: &str, args: &[&str]) -> Result<(Self, Child)> {
605 let mut child = Command::new(cmd)
606 .args(args)
607 .stdin(Stdio::piped())
608 .stdout(Stdio::piped())
609 .stderr(Stdio::null())
610 .spawn()
611 .map_err(|e| {
612 if e.kind() == std::io::ErrorKind::NotFound {
613 anyhow::anyhow!(missing_server_message(cmd))
614 } else {
615 anyhow::anyhow!("cannot start `{cmd}`: {e:#}")
616 }
617 })?;
618 let stdin = child.stdin.take().context("server stdin unavailable")?;
619 let stdout = child.stdout.take().context("server stdout unavailable")?;
620 let (tx, inbox) = mpsc::channel();
621 std::thread::spawn(move || {
622 let mut framing = FramingReader::new(BufReader::new(stdout));
623 while let Ok(msg) = framing.read_message() {
624 if tx.send(msg).is_err() {
625 break;
626 }
627 }
628 });
629 Ok((
630 Self {
631 stdin,
632 inbox,
633 dead: false,
634 },
635 child,
636 ))
637 }
638}
639
640impl RpcTransport for StdioTransport {
641 fn send(&mut self, body: &Value) -> Result<()> {
642 let bytes = encode_message(body);
643 self.stdin.write_all(&bytes).context("server stdin closed")
644 }
645
646 fn drain(&mut self) -> Vec<Value> {
647 let mut out = Vec::new();
648 loop {
649 match self.inbox.try_recv() {
650 Ok(msg) => out.push(msg),
651 Err(mpsc::TryRecvError::Empty) => break,
652 Err(mpsc::TryRecvError::Disconnected) => {
653 self.dead = true;
654 break;
655 }
656 }
657 }
658 out
659 }
660
661 fn alive(&mut self) -> bool {
662 !self.dead
663 }
664}
665
666pub struct Bridge {
669 next_id: i64,
670 pending: HashMap<i64, PendingKind>,
671 responses: HashMap<i64, Value>,
672 versions: HashMap<String, i32>,
673 completions: VecDeque<(i64, Vec<CompletionItem>)>,
674 hovers: VecDeque<(i64, String)>,
675 signatures: VecDeque<(i64, Vec<String>)>,
676 notices: VecDeque<String>,
677 pub diagnostics: HashMap<String, Vec<Diagnostic>>,
678}
679
680impl Bridge {
681 pub fn new() -> Self {
682 Self {
683 next_id: 1,
684 pending: HashMap::new(),
685 responses: HashMap::new(),
686 versions: HashMap::new(),
687 completions: VecDeque::new(),
688 hovers: VecDeque::new(),
689 signatures: VecDeque::new(),
690 notices: VecDeque::new(),
691 diagnostics: HashMap::new(),
692 }
693 }
694
695 fn call(&mut self, tx: &mut dyn RpcTransport, kind: PendingKind, body: Value) -> Result<i64> {
696 let id = self.next_id;
697 self.next_id += 1;
698 tx.send(&body)?;
699 self.pending.insert(id, kind);
700 Ok(id)
701 }
702
703 pub fn send_initialize(
704 &mut self,
705 tx: &mut dyn RpcTransport,
706 root_uri: Option<&str>,
707 ) -> Result<i64> {
708 let id = self.next_id;
709 let body = initialize_request(id, root_uri);
710 self.call(tx, PendingKind::Initialize, body)
711 }
712
713 pub fn send_initialized(&mut self, tx: &mut dyn RpcTransport) -> Result<()> {
714 tx.send(&initialized_notification())
715 }
716
717 pub fn send_configuration(
718 &mut self,
719 tx: &mut dyn RpcTransport,
720 stubs: Option<&Path>,
721 ) -> Result<()> {
722 tx.send(&did_change_configuration(stubs))
723 }
724
725 pub fn did_open(&mut self, tx: &mut dyn RpcTransport, uri: &str, text: &str) -> Result<()> {
726 self.versions.insert(uri.to_string(), 1);
727 tx.send(&did_open_notification(uri, text))
728 }
729
730 pub fn did_change(&mut self, tx: &mut dyn RpcTransport, uri: &str, text: &str) -> Result<()> {
731 let version = self.versions.entry(uri.to_string()).or_insert(0);
732 *version += 1;
733 tx.send(&did_change_notification(uri, *version, text))
734 }
735
736 pub fn request_completion(
737 &mut self,
738 tx: &mut dyn RpcTransport,
739 uri: &str,
740 line: u32,
741 character: u32,
742 ) -> Result<i64> {
743 let id = self.next_id;
744 let body = completion_request(id, uri, line, character);
745 self.call(tx, PendingKind::Completion, body)
746 }
747
748 pub fn request_hover(
749 &mut self,
750 tx: &mut dyn RpcTransport,
751 uri: &str,
752 line: u32,
753 character: u32,
754 ) -> Result<i64> {
755 let id = self.next_id;
756 let body = hover_request(id, uri, line, character);
757 self.call(tx, PendingKind::Hover, body)
758 }
759
760 pub fn request_signature(
761 &mut self,
762 tx: &mut dyn RpcTransport,
763 uri: &str,
764 line: u32,
765 character: u32,
766 ) -> Result<i64> {
767 let id = self.next_id;
768 let body = signature_request(id, uri, line, character);
769 self.call(tx, PendingKind::Signature, body)
770 }
771
772 pub fn ingest(&mut self, msg: Value, tx: &mut dyn RpcTransport, stubs: Option<&Path>) {
775 if let Some(id) = msg.get("id").and_then(Value::as_i64) {
776 if msg.get("method").is_some() {
777 self.answer_server_request(id, &msg, tx, stubs);
778 } else {
779 let result = msg.get("result").cloned().unwrap_or(Value::Null);
780 match self.pending.remove(&id) {
781 Some(PendingKind::Completion) => {
782 self.completions
783 .push_back((id, parse_completion_items(&result)));
784 }
785 Some(PendingKind::Hover) => {
786 self.hovers.push_back((id, parse_hover(&result)));
787 }
788 Some(PendingKind::Signature) => {
789 self.signatures
790 .push_back((id, parse_signature_help(&result)));
791 }
792 Some(PendingKind::Initialize) => {
793 self.responses.insert(id, result);
794 }
795 None => {}
796 }
797 }
798 return;
799 }
800 let Some(method) = msg.get("method").and_then(Value::as_str) else {
801 return;
802 };
803 match method {
804 "textDocument/publishDiagnostics" => {
805 let params = msg.get("params").unwrap_or(&Value::Null);
806 let uri = params.get("uri").and_then(Value::as_str).unwrap_or("");
807 self.diagnostics
808 .insert(uri.to_string(), parse_publish_diagnostics(params));
809 }
810 "window/showMessage" | "window/logMessage" => {
811 if let Some(text) = msg
812 .get("params")
813 .and_then(|p| p.get("message"))
814 .and_then(Value::as_str)
815 {
816 self.notices.push_back(text.to_string());
817 }
818 }
819 _ => {}
820 }
821 }
822
823 fn answer_server_request(
824 &mut self,
825 id: i64,
826 msg: &Value,
827 tx: &mut dyn RpcTransport,
828 stubs: Option<&Path>,
829 ) {
830 let method = msg.get("method").and_then(Value::as_str).unwrap_or("");
831 let reply = match method {
832 "workspace/configuration" => {
833 let section = msg
834 .pointer("/params/items/0/section")
835 .and_then(Value::as_str)
836 .unwrap_or("");
837 configuration_response(id, section, stubs)
838 }
839 _ => serde_json::json!({"jsonrpc": "2.0", "id": id, "result": null}),
840 };
841 let _ = tx.send(&reply);
842 }
843
844 pub fn poll(&mut self, tx: &mut dyn RpcTransport, stubs: Option<&Path>) -> bool {
847 for msg in tx.drain() {
848 self.ingest(msg, tx, stubs);
849 }
850 tx.alive()
851 }
852
853 pub fn await_response(
856 &mut self,
857 tx: &mut dyn RpcTransport,
858 id: i64,
859 stubs: Option<&Path>,
860 timeout: Duration,
861 ) -> Result<Value> {
862 let start = Instant::now();
863 loop {
864 for msg in tx.drain() {
865 self.ingest(msg, tx, stubs);
866 }
867 if let Some(result) = self.responses.remove(&id) {
868 return Ok(result);
869 }
870 if !tx.alive() {
871 anyhow::bail!("server exited during initialize");
872 }
873 if start.elapsed() > timeout {
874 anyhow::bail!("server did not answer initialize in time");
875 }
876 std::thread::sleep(Duration::from_millis(10));
877 }
878 }
879
880 pub fn take_completion(&mut self) -> Option<(i64, Vec<CompletionItem>)> {
881 self.completions.pop_front()
882 }
883
884 pub fn take_hover(&mut self) -> Option<(i64, String)> {
885 self.hovers.pop_front()
886 }
887
888 pub fn take_signature(&mut self) -> Option<(i64, Vec<String>)> {
889 self.signatures.pop_front()
890 }
891
892 pub fn take_notice(&mut self) -> Option<String> {
893 self.notices.pop_front()
894 }
895}
896
897impl Default for Bridge {
898 fn default() -> Self {
899 Self::new()
900 }
901}
902
903pub struct ChildGuard(Child);
905
906impl ChildGuard {
907 pub fn new(child: Child) -> Self {
908 Self(child)
909 }
910}
911
912impl Drop for ChildGuard {
913 fn drop(&mut self) {
914 let _ = self.0.kill();
915 let _ = self.0.wait();
916 }
917}
918
919pub struct LspClient {
921 bridge: Bridge,
922 transport: Box<dyn RpcTransport>,
923 _child: Option<ChildGuard>,
924 stubs_dir: Option<PathBuf>,
925 dead_reported: bool,
926 pub server_version: Option<String>,
927}
928
929impl LspClient {
930 fn new(
931 transport: Box<dyn RpcTransport>,
932 child: Option<ChildGuard>,
933 stubs_dir: Option<PathBuf>,
934 server_version: Option<String>,
935 ) -> Self {
936 Self {
937 bridge: Bridge::new(),
938 transport,
939 _child: child,
940 stubs_dir,
941 dead_reported: false,
942 server_version,
943 }
944 }
945
946 pub fn poll(&mut self) -> Option<String> {
948 let stubs = self.stubs_dir.clone();
949 if !self.bridge.poll(&mut *self.transport, stubs.as_deref()) && !self.dead_reported {
950 self.dead_reported = true;
951 return Some("LSP: language server exited; editing continues without it.".to_string());
952 }
953 None
954 }
955
956 pub fn did_open(&mut self, uri: &str, text: &str) -> Result<()> {
957 self.bridge.did_open(&mut *self.transport, uri, text)
958 }
959
960 pub fn did_change(&mut self, uri: &str, text: &str) -> Result<()> {
961 self.bridge.did_change(&mut *self.transport, uri, text)
962 }
963
964 pub fn request_completion(&mut self, uri: &str, line: u32, character: u32) -> Result<i64> {
965 self.bridge
966 .request_completion(&mut *self.transport, uri, line, character)
967 }
968
969 pub fn request_hover(&mut self, uri: &str, line: u32, character: u32) -> Result<i64> {
970 self.bridge
971 .request_hover(&mut *self.transport, uri, line, character)
972 }
973
974 pub fn request_signature(&mut self, uri: &str, line: u32, character: u32) -> Result<i64> {
975 self.bridge
976 .request_signature(&mut *self.transport, uri, line, character)
977 }
978
979 pub fn take_completion(&mut self) -> Option<(i64, Vec<CompletionItem>)> {
980 self.bridge.take_completion()
981 }
982
983 pub fn take_hover(&mut self) -> Option<(i64, String)> {
984 self.bridge.take_hover()
985 }
986
987 pub fn take_signature(&mut self) -> Option<(i64, Vec<String>)> {
988 self.bridge.take_signature()
989 }
990
991 pub fn take_notice(&mut self) -> Option<String> {
992 self.bridge.take_notice()
993 }
994
995 pub fn diagnostics_for_uri(&self, uri: &str) -> &[Diagnostic] {
996 self.bridge
997 .diagnostics
998 .get(uri)
999 .map(Vec::as_slice)
1000 .unwrap_or(&[])
1001 }
1002
1003 pub fn diagnostic_count(&self) -> usize {
1004 self.bridge.diagnostics.values().map(Vec::len).sum()
1005 }
1006}
1007
1008pub enum LspOutcome {
1010 Ready {
1011 client: LspClient,
1012 warning: Option<String>,
1013 },
1014 Failed(String),
1015}
1016
1017pub fn spawn_lsp(root_dir: Option<PathBuf>, script: &Path) -> Receiver<LspOutcome> {
1021 let (tx, rx) = mpsc::channel();
1022 let script = script.to_path_buf();
1023 std::thread::spawn(move || {
1024 let outcome = start_blocking(root_dir.as_deref(), &script);
1025 let _ = tx.send(outcome);
1026 });
1027 rx
1028}
1029
1030fn start_blocking(root_dir: Option<&Path>, script: &Path) -> LspOutcome {
1031 let cmd = std::env::var(ENV_CMD_OVERRIDE).unwrap_or_else(|_| SERVER_CMD.to_string());
1032 let (server_version, mut warning) = probe_server_version(&cmd);
1033 let stubs_dir = root_dir
1034 .and_then(find_stubs_dir)
1035 .or_else(|| find_stubs_dir(script))
1036 .or_else(|| find_stubs_dir(Path::new(".")))
1037 .or_else(materialized_stubs_dir);
1038 if stubs_dir.is_none() {
1039 let w = "LSP: shipped pite.pyi stubs not found; `import pite` may not resolve.".to_string();
1040 warning = Some(match warning {
1041 Some(prev) => format!("{prev} {w}"),
1042 None => w,
1043 });
1044 }
1045 let (mut transport, child) = match StdioTransport::spawn(&cmd, SERVER_ARGS) {
1046 Ok(pair) => pair,
1047 Err(e) => return LspOutcome::Failed(format!("LSP unavailable: {e:#}")),
1048 };
1049 let root_uri = root_dir.map(path_to_uri);
1050 let mut bridge = Bridge::new();
1051 let init_id = match bridge.send_initialize(&mut transport, root_uri.as_deref()) {
1052 Ok(id) => id,
1053 Err(e) => return LspOutcome::Failed(format!("LSP: cannot talk to server: {e:#}")),
1054 };
1055 let stubs = stubs_dir.clone();
1056 match bridge.await_response(&mut transport, init_id, stubs.as_deref(), INIT_TIMEOUT) {
1057 Ok(_) => {}
1058 Err(e) => {
1059 return LspOutcome::Failed(format!(
1060 "LSP: initialize failed ({e:#}); editing continues without it."
1061 ))
1062 }
1063 }
1064 let stubs_ref = stubs_dir.clone();
1065 if bridge.send_initialized(&mut transport).is_err()
1066 || bridge
1067 .send_configuration(&mut transport, stubs_ref.as_deref())
1068 .is_err()
1069 {
1070 return LspOutcome::Failed("LSP: server went quiet after initialize.".to_string());
1071 }
1072 let mut client = LspClient::new(
1073 Box::new(transport),
1074 Some(ChildGuard::new(child)),
1075 stubs_dir,
1076 server_version,
1077 );
1078 client.bridge = bridge;
1079 LspOutcome::Ready { client, warning }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085 use std::io::Cursor;
1086
1087 struct MockServer {
1089 opened: HashMap<String, String>,
1090 }
1091
1092 impl MockServer {
1093 fn new() -> Self {
1094 Self {
1095 opened: HashMap::new(),
1096 }
1097 }
1098
1099 fn handle(&mut self, msg: &Value) -> Vec<Value> {
1100 let method = msg.get("method").and_then(Value::as_str).unwrap_or("");
1101 if msg.get("id").is_some() {
1102 let id = msg.get("id").cloned().unwrap();
1103 let result = match method {
1104 "initialize" => serde_json::json!({"capabilities": {}}),
1105 "textDocument/completion" => serde_json::json!([
1106 {"label": "position", "detail": "tuple[float, float]"},
1107 {"label": "pressed", "detail": "def pressed"},
1108 ]),
1109 "textDocument/hover" => serde_json::json!({"contents": {
1110 "kind": "markdown", "value": "**position**: tuple"}}),
1111 "textDocument/signatureHelp" => serde_json::json!({"signatures": [
1112 {"label": "play(path, volume)"},
1113 ]}),
1114 "shutdown" => Value::Null,
1115 _ => Value::Null,
1116 };
1117 return vec![serde_json::json!({"jsonrpc": "2.0", "id": id, "result": result})];
1118 }
1119 match method {
1120 "textDocument/didOpen" | "textDocument/didChange" => {
1121 let doc = msg.get("params").and_then(|p| p.get("textDocument"));
1122 let uri = doc
1123 .and_then(|d| d.get("uri"))
1124 .and_then(Value::as_str)
1125 .unwrap_or("");
1126 let text = if method.ends_with("didOpen") {
1127 msg.pointer("/params/textDocument/text")
1128 .and_then(Value::as_str)
1129 .unwrap_or("")
1130 } else {
1131 msg.pointer("/params/contentChanges/0/text")
1132 .and_then(Value::as_str)
1133 .unwrap_or("")
1134 };
1135 self.opened.insert(uri.to_string(), text.to_string());
1136 let diagnostics = if text.contains("oops") {
1137 serde_json::json!([{"range": {"start": {"line": 0, "character": 0},
1138 "end": {"line": 0, "character": 4}},
1139 "severity": 1, "message": "mock: undefined name"}])
1140 } else {
1141 serde_json::json!([])
1142 };
1143 vec![serde_json::json!({"jsonrpc": "2.0",
1144 "method": "textDocument/publishDiagnostics",
1145 "params": {"uri": uri, "diagnostics": diagnostics}})]
1146 }
1147 _ => Vec::new(),
1148 }
1149 }
1150 }
1151
1152 struct MockTransport {
1153 pub outbox: Vec<Value>,
1154 inbox: VecDeque<Value>,
1155 server: MockServer,
1156 }
1157
1158 impl MockTransport {
1159 fn new() -> Self {
1160 Self {
1161 outbox: Vec::new(),
1162 inbox: VecDeque::new(),
1163 server: MockServer::new(),
1164 }
1165 }
1166 }
1167
1168 impl RpcTransport for MockTransport {
1169 fn send(&mut self, body: &Value) -> Result<()> {
1170 self.outbox.push(body.clone());
1171 self.inbox.extend(self.server.handle(body));
1172 Ok(())
1173 }
1174
1175 fn drain(&mut self) -> Vec<Value> {
1176 self.inbox.drain(..).collect()
1177 }
1178
1179 fn alive(&mut self) -> bool {
1180 true
1181 }
1182 }
1183
1184 #[test]
1185 fn framing_round_trip() {
1186 let body = serde_json::json!({"jsonrpc": "2.0", "id": 7, "method": "x", "params": {}});
1187 let bytes = encode_message(&body);
1188 assert!(bytes.starts_with(b"Content-Length: "));
1189 let mut reader = FramingReader::new(Cursor::new(bytes));
1190 assert_eq!(reader.read_message().unwrap(), body);
1191 }
1192
1193 #[test]
1194 fn framing_rejects_garbage_loudly() {
1195 let mut reader = FramingReader::new(Cursor::new(b"not a frame".to_vec()));
1196 assert!(reader.read_message().is_err());
1197 let mut reader = FramingReader::new(Cursor::new(b"Content-Length: 5\r\n\r\n###".to_vec()));
1198 assert!(reader.read_message().is_err());
1199 }
1200
1201 #[test]
1202 fn builders_carry_params() {
1203 let open = did_open_notification("file:///a.py", "import pite\n");
1204 assert_eq!(
1205 open.pointer("/params/textDocument/uri").unwrap(),
1206 "file:///a.py"
1207 );
1208 let change = did_change_notification("file:///a.py", 7, "x = 1\n");
1209 assert_eq!(
1210 change.pointer("/params/contentChanges/0/text").unwrap(),
1211 "x = 1\n"
1212 );
1213 assert_eq!(change.pointer("/params/textDocument/version").unwrap(), 7);
1214 let req = completion_request(3, "file:///a.py", 4, 9);
1215 assert_eq!(req.get("id").unwrap(), 3);
1216 assert_eq!(req.pointer("/params/position/line").unwrap(), 4);
1217 let init = initialize_request(1, Some("file:///proj"));
1218 assert_eq!(init.pointer("/params/rootUri").unwrap(), "file:///proj");
1219 assert_eq!(
1220 init.pointer("/params/workspaceFolders/0/uri").unwrap(),
1221 "file:///proj"
1222 );
1223 assert_eq!(
1224 init.pointer("/params/capabilities/workspace/configuration"),
1225 Some(&Value::Bool(true))
1226 );
1227 let bare = initialize_request(1, None);
1228 assert_eq!(
1229 bare.pointer("/params/workspaceFolders").unwrap(),
1230 &serde_json::json!([])
1231 );
1232 let cfg = did_change_configuration(Some(Path::new("/stubs")));
1233 assert_eq!(
1234 cfg.pointer("/params/settings/python/analysis/extraPaths/0")
1235 .unwrap(),
1236 "/stubs"
1237 );
1238 }
1239
1240 #[test]
1241 fn mock_round_trip_open_change_completion_diagnostics() {
1242 let mut bridge = Bridge::new();
1243 let mut tx = MockTransport::new();
1244 let uri = "file:///proj/scripts/player.py";
1245
1246 bridge
1247 .did_open(&mut tx, uri, "import pite\noops\n")
1248 .unwrap();
1249 assert!(bridge.poll(&mut tx, None));
1250 let diags = bridge.diagnostics.get(uri).cloned().unwrap_or_default();
1251 assert_eq!(diags.len(), 1);
1252 assert_eq!(diags[0].line, 0);
1253 assert!(diags[0].message.contains("undefined name"));
1254
1255 bridge.did_change(&mut tx, uri, "import pite\n").unwrap();
1256 assert!(bridge.poll(&mut tx, None));
1257 assert!(bridge
1258 .diagnostics
1259 .get(uri)
1260 .cloned()
1261 .unwrap_or_default()
1262 .is_empty());
1263 let versions: Vec<i64> = tx
1264 .outbox
1265 .iter()
1266 .filter(|m| m.get("method").and_then(Value::as_str) == Some("textDocument/didChange"))
1267 .filter_map(|m| {
1268 m.pointer("/params/textDocument/version")
1269 .and_then(Value::as_i64)
1270 })
1271 .collect();
1272 assert_eq!(versions, vec![2]);
1273
1274 let id = bridge.request_completion(&mut tx, uri, 1, 2).unwrap();
1275 assert!(bridge.poll(&mut tx, None));
1276 let (got_id, items) = bridge.take_completion().expect("completion arrives");
1277 assert_eq!(got_id, id);
1278 assert_eq!(items.len(), 2);
1279 assert_eq!(items[0].label, "position");
1280
1281 bridge.request_hover(&mut tx, uri, 1, 2).unwrap();
1282 assert!(bridge.poll(&mut tx, None));
1283 let (_, hover) = bridge.take_hover().expect("hover arrives");
1284 assert!(hover.contains("position"));
1285
1286 bridge.request_signature(&mut tx, uri, 1, 2).unwrap();
1287 assert!(bridge.poll(&mut tx, None));
1288 let (_, sigs) = bridge.take_signature().expect("signature arrives");
1289 assert_eq!(sigs, vec!["play(path, volume)"]);
1290
1291 assert!(tx
1293 .outbox
1294 .iter()
1295 .any(|m| m.get("method").unwrap() == "textDocument/didOpen"));
1296 }
1297
1298 #[test]
1299 fn server_requests_get_answers() {
1300 let mut bridge = Bridge::new();
1301 let mut tx = MockTransport::new();
1302 bridge.ingest(
1303 serde_json::json!({"jsonrpc": "2.0", "id": 9,
1304 "method": "workspace/configuration",
1305 "params": {"items": [{"scopeUri": "file:///proj", "section": "python"}]}}),
1306 &mut tx,
1307 Some(Path::new("/stubs")),
1308 );
1309 let reply = tx.outbox.last().expect("server request answered");
1310 assert_eq!(reply.get("id").unwrap(), 9);
1311 assert_eq!(
1312 reply.pointer("/result/0/analysis/extraPaths/0").unwrap(),
1313 "/stubs"
1314 );
1315 }
1316
1317 #[test]
1318 fn missing_server_fails_loudly() {
1319 let err = StdioTransport::spawn("definitely-not-a-pite-lsp-server", SERVER_ARGS)
1320 .err()
1321 .expect("missing server must fail");
1322 let msg = format!("{err:#}");
1323 assert!(msg.contains("not found on PATH"), "got: {msg}");
1324 assert!(msg.contains(PINNED_PYRIGHT), "got: {msg}");
1325 assert!(msg.contains(ENV_CMD_OVERRIDE), "got: {msg}");
1326 }
1327
1328 #[test]
1329 fn version_parse_and_compare() {
1330 assert_eq!(parse_version("pyright 1.1.411"), Some((1, 1, 411)));
1331 assert_eq!(parse_version("1.1.398"), Some((1, 1, 398)));
1332 assert_eq!(parse_version("no version here"), None);
1333 assert!(version_at_least((1, 1, 411), (1, 1, 411)));
1334 assert!(version_at_least((1, 1, 412), (1, 1, 411)));
1335 assert!(!version_at_least((1, 1, 400), (1, 1, 411)));
1336 }
1337
1338 #[test]
1339 fn configuration_answers_section_values() {
1340 let py = configuration_response(7, "python", Some(Path::new("/stubs")));
1344 assert_eq!(
1345 py.pointer("/result/0/analysis/extraPaths/0").unwrap(),
1346 "/stubs"
1347 );
1348 assert!(py.pointer("/result/0/python").is_none());
1349 let other = configuration_response(8, "pyright", Some(Path::new("/stubs")));
1350 assert_eq!(other.pointer("/result/0").unwrap(), &serde_json::json!({}));
1351 }
1352
1353 #[test]
1354 fn stubs_found_beside_layout() {
1355 let dir = std::env::temp_dir().join(format!("pite-lsp-stubs-{}", std::process::id()));
1356 let _ = std::fs::remove_dir_all(&dir);
1357 let stubs = dir.join("python").join("pite");
1358 std::fs::create_dir_all(&stubs).unwrap();
1359 std::fs::write(stubs.join(STUBS_FILE), "x: int\n").unwrap();
1360 let script = dir.join("scripts").join("a.py");
1361 assert_eq!(find_stubs_dir(&script), Some(stubs));
1362 assert!(find_stubs_dir(Path::new("/definitely/not/here")).is_none());
1363
1364 std::fs::remove_dir_all(&dir).ok();
1365 }
1366
1367 #[test]
1368 fn uris_are_well_formed_on_every_platform() {
1369 assert_eq!(
1370 uri_from_path_text("/home/dev/pite/scripts/a.py"),
1371 "file:///home/dev/pite/scripts/a.py"
1372 );
1373 assert_eq!(
1376 uri_from_path_text(r"D:\Projects\Pite\pite\examples\minimal-2d"),
1377 "file:///D:/Projects/Pite/pite/examples/minimal-2d"
1378 );
1379 assert_eq!(
1380 uri_from_path_text("D:/Projects/Pite/pite/scripts/a.py"),
1381 "file:///D:/Projects/Pite/pite/scripts/a.py"
1382 );
1383 assert_eq!(
1384 uri_from_path_text("/home/dev/my game/a b.py"),
1385 "file:///home/dev/my%20game/a%20b.py"
1386 );
1387 assert_eq!(path_to_uri(Path::new("/tmp/a.py")), "file:///tmp/a.py");
1388 }
1389
1390 #[test]
1391 fn positions_and_word_starts() {
1392 let text = "ab\ncλd\n";
1393 assert_eq!(offset_to_position(text, 0), (0, 0));
1394 assert_eq!(offset_to_position(text, 3), (1, 0));
1395 assert_eq!(offset_to_position(text, 4), (1, 1));
1397 assert_eq!(byte_offset_of_char(text, 0), 0);
1398 assert_eq!(byte_offset_of_char(text, 4), 4);
1399 assert_eq!(word_start_before("self.position", 13), 5);
1400 assert_eq!(word_start_before("self.position", 4), 0);
1401 assert_eq!(word_start_before("x = 1", 4), 4);
1402 assert_eq!(word_start_before("x = 1", 5), 4);
1403 assert_eq!(word_end_after("self.position", 5), 13);
1404 assert_eq!(word_end_after("self.position", 0), 4);
1405 assert_eq!(word_end_after("x = 1", 4), 5);
1406 assert_eq!(word_end_after("x = 1", 5), 5);
1407 }
1408
1409 #[test]
1410 fn line_starts_land() {
1411 let text = "a\nbb\n\nc";
1412 assert_eq!(line_start_byte(text, 0), 0);
1413 assert_eq!(line_start_byte(text, 1), 0);
1414 assert_eq!(line_start_byte(text, 2), 2);
1415 assert_eq!(line_start_byte(text, 3), 5);
1416 assert_eq!(line_start_byte(text, 4), 6);
1417 assert_eq!(line_start_byte(text, 99), text.len());
1418 }
1419
1420 #[test]
1421 fn embedded_stubs_materialize_and_repair() {
1422 let dir = std::env::temp_dir().join(format!("pite-lsp-stubtest-{}", std::process::id()));
1423 let _ = std::fs::remove_dir_all(&dir);
1424 let back = write_stubs_to(&dir).unwrap();
1425 assert_eq!(back, dir);
1426 assert_eq!(
1427 std::fs::read_to_string(dir.join(STUBS_FILE)).unwrap(),
1428 EMBEDDED_STUBS
1429 );
1430 assert_eq!(
1431 std::fs::read_to_string(dir.join("pite.py")).unwrap(),
1432 EMBEDDED_SOURCE
1433 );
1434 std::fs::write(dir.join(STUBS_FILE), "stale").unwrap();
1435 write_stubs_to(&dir).unwrap();
1436 assert_eq!(
1437 std::fs::read_to_string(dir.join(STUBS_FILE)).unwrap(),
1438 EMBEDDED_STUBS
1439 );
1440 let _ = std::fs::remove_dir_all(&dir);
1441 }
1442
1443 #[test]
1444 fn null_results_are_empty() {
1445 assert!(parse_completion_items(&Value::Null).is_empty());
1446 assert!(parse_hover(&Value::Null).is_empty());
1447 assert!(parse_signature_help(&Value::Null).is_empty());
1448 assert!(parse_publish_diagnostics(&Value::Null).is_empty());
1449 }
1450
1451 #[cfg(unix)]
1452 #[test]
1453 fn framing_survives_real_fds() {
1454 use std::os::unix::net::UnixStream;
1455 let (a, b) = UnixStream::pair().unwrap();
1456 let body = serde_json::json!({"jsonrpc": "2.0", "method": "ping"});
1457 let bytes = encode_message(&body);
1458 let mut writer = a;
1459 writer.write_all(&bytes).unwrap();
1460 let mut reader = FramingReader::new(BufReader::new(b));
1461 assert_eq!(reader.read_message().unwrap(), body);
1462 }
1463
1464 #[test]
1469 fn live_pyright_smoke_open_completion_change() {
1470 if std::env::var("PITE_LSP_SMOKE").is_err() {
1471 eprintln!("skipped: set PITE_LSP_SMOKE=1 for the live pyright smoke");
1472 return;
1473 }
1474 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1475 .join("..")
1476 .join("examples")
1477 .join("minimal-2d");
1478 let script = root.join("scripts").join("player.py");
1479 let text = std::fs::read_to_string(&script).expect("dogfood script exists");
1480 let rx = spawn_lsp(Some(root), &script);
1481 let mut client = match rx
1482 .recv_timeout(Duration::from_secs(180))
1483 .expect("starter answers")
1484 {
1485 LspOutcome::Ready { client, warning } => {
1486 if let Some(w) = warning {
1487 eprintln!("smoke warning: {w}");
1488 }
1489 assert_eq!(client.server_version.as_deref(), Some(PINNED_PYRIGHT));
1490 client
1491 }
1492 LspOutcome::Failed(msg) => panic!("smoke server failed: {msg}"),
1493 };
1494 let uri = path_to_uri(&script);
1495 client.did_open(&uri, &text).expect("didOpen sends");
1496
1497 while let Some(notice) = client.take_notice() {
1501 assert!(
1502 !notice.contains("<default workspace root>"),
1503 "smoke: server ignored the project root: {notice}"
1504 );
1505 }
1506
1507 let (dot_line, dot_col) = text
1509 .lines()
1510 .enumerate()
1511 .find_map(|(l, line)| {
1512 let col = line.find("self.")? + 5;
1513 Some((l as u32, col as u32))
1514 })
1515 .expect("smoke script contains `self.`");
1516 let found = smoke_await_completion(&mut client, &uri, dot_line, dot_col);
1517 assert!(!found.is_empty(), "smoke: completion came back empty");
1518 assert!(
1519 found.iter().any(|i| i.label == "position"),
1520 "smoke: expected `position`, got {:?}",
1521 found.iter().map(|i| &i.label).collect::<Vec<_>>()
1522 );
1523
1524 let broken = format!("{text}\nundefined_smoke_name_xyz\n");
1527 client.did_change(&uri, &broken).expect("didChange sends");
1528 let after = smoke_await_completion(&mut client, &uri, dot_line, dot_col);
1529 assert!(
1530 after.iter().any(|i| i.label == "position"),
1531 "smoke: completion broke after didChange, got {:?}",
1532 after.iter().map(|i| &i.label).collect::<Vec<_>>()
1533 );
1534 }
1535
1536 fn smoke_await_completion(
1539 client: &mut LspClient,
1540 uri: &str,
1541 line: u32,
1542 character: u32,
1543 ) -> Vec<CompletionItem> {
1544 client
1545 .request_completion(uri, line, character)
1546 .expect("completion sends");
1547 let deadline = Instant::now() + Duration::from_secs(120);
1548 loop {
1549 if let Some(msg) = client.poll() {
1550 panic!("smoke: server died ({msg})");
1551 }
1552 if let Some((_, items)) = client.take_completion() {
1553 return items;
1554 }
1555 if Instant::now() > deadline {
1556 panic!("smoke: no completion arrived");
1557 }
1558 std::thread::sleep(Duration::from_millis(100));
1559 }
1560 }
1561}