1use std::collections::{HashMap, HashSet};
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11use std::sync::Arc;
12use std::time::Duration;
13
14use shape_ast::ast::{ForeignFunctionDef, Item, Span};
15use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16use tokio::process::{Child, Command};
17use tokio::sync::Mutex;
18use tower_lsp_server::ls_types::{
19 CompletionItem, CompletionItemKind, Diagnostic, DiagnosticSeverity, GotoDefinitionResponse,
20 Hover, Location, LocationLink, Position, Range, SemanticTokens, SignatureHelp, Uri,
21};
22
23#[derive(Clone, Debug)]
29pub struct PositionMap {
30 line_mapping: Vec<Option<ForeignLineMapping>>,
33 body_start_offset: usize,
35 header_lines: u32,
37}
38
39#[derive(Clone, Copy, Debug)]
40struct ForeignLineMapping {
41 source_line: u32,
42 source_col_start: u32,
43 virtual_col_start: u32,
44}
45
46impl PositionMap {
47 pub fn new(body_span: Span, source: &str) -> Self {
48 let body_start_offset = body_span.start;
49 let mut line_mapping = Vec::new();
50
51 let (body_start_line, body_start_col) =
52 crate::util::offset_to_line_col(source, body_span.start.min(source.len()));
53
54 let body_end = body_span.end.min(source.len());
55 let body_text = &source[body_span.start.min(source.len())..body_end];
56 for (i, _) in body_text.split('\n').enumerate() {
57 line_mapping.push(Some(ForeignLineMapping {
58 source_line: body_start_line + i as u32,
59 source_col_start: if i == 0 { body_start_col } else { 0 },
60 virtual_col_start: 0,
61 }));
62 }
63
64 Self {
65 line_mapping,
66 body_start_offset,
67 header_lines: 0,
68 }
69 }
70
71 pub fn shape_to_virtual(&self, pos: Position) -> Option<Position> {
73 for (virtual_line, mapping) in self.line_mapping.iter().enumerate() {
74 if let Some(mapping) = mapping {
75 if mapping.source_line == pos.line {
76 let character = if pos.character >= mapping.source_col_start {
77 mapping.virtual_col_start + (pos.character - mapping.source_col_start)
78 } else {
79 mapping
80 .virtual_col_start
81 .saturating_sub(mapping.source_col_start - pos.character)
82 };
83 return Some(Position {
84 line: virtual_line as u32,
85 character,
86 });
87 }
88 }
89 }
90 None
91 }
92
93 pub fn virtual_to_shape(&self, pos: Position) -> Option<Position> {
95 let virtual_line = pos.line as usize;
96 if virtual_line < self.line_mapping.len() {
97 if let Some(mapping) = self.line_mapping[virtual_line] {
98 let character = if pos.character >= mapping.virtual_col_start {
99 mapping.source_col_start + (pos.character - mapping.virtual_col_start)
100 } else {
101 mapping
102 .source_col_start
103 .saturating_sub(mapping.virtual_col_start - pos.character)
104 };
105 return Some(Position {
106 line: mapping.source_line,
107 character,
108 });
109 }
110 }
111 None
112 }
113
114 pub fn virtual_range_to_shape(&self, range: Range) -> Option<Range> {
116 let start = self.virtual_to_shape(range.start)?;
117 let end = self.virtual_to_shape(range.end)?;
118 Some(Range { start, end })
119 }
120
121 pub fn body_start_offset(&self) -> usize {
123 self.body_start_offset
124 }
125
126 pub fn header_lines(&self) -> u32 {
128 self.header_lines
129 }
130}
131
132pub struct VirtualDocument {
138 pub content: String,
140 pub position_map: PositionMap,
142 pub language: String,
144 pub function_name: String,
146 pub source_uri: String,
148 pub virtual_path: PathBuf,
150}
151
152pub fn generate_virtual_document(
154 def: &ForeignFunctionDef,
155 source: &str,
156 source_uri: &str,
157 workspace_dir: &Path,
158 file_extension: &str,
159) -> VirtualDocument {
160 let params: Vec<String> = def
161 .params
162 .iter()
163 .flat_map(|p| p.get_identifiers())
164 .collect();
165
166 let header = if def.is_async {
167 format!("async def {}({}):\n", def.name, params.join(", "))
168 } else {
169 format!("def {}({}):\n", def.name, params.join(", "))
170 };
171 let body = &def.body_text;
172 let body_lines: Vec<&str> = body.lines().collect();
173
174 let mut virtual_doc = header;
175 for line in &body_lines {
176 virtual_doc.push_str(" ");
177 virtual_doc.push_str(line);
178 virtual_doc.push('\n');
179 }
180 if body.trim().is_empty() {
181 virtual_doc.push_str(" pass\n");
182 }
183
184 let mut line_mapping = Vec::new();
185 line_mapping.push(None);
187
188 let (body_start_line, body_start_col) =
189 crate::util::offset_to_line_col(source, def.body_span.start.min(source.len()));
190 let body_raw_end = def.body_span.end.min(source.len());
191 let body_raw = &source[def.body_span.start.min(source.len())..body_raw_end];
192 let body_raw_lines: Vec<&str> = body_raw.lines().collect();
193
194 for (i, body_line) in body_lines.iter().enumerate() {
195 let raw_line = body_raw_lines.get(i).copied().unwrap_or_default();
196 let source_line_base_col = if i == 0 { body_start_col } else { 0 };
197 let dedent_prefix_chars = dedented_prefix_chars(raw_line, body_line);
198 line_mapping.push(Some(ForeignLineMapping {
199 source_line: body_start_line + i as u32,
200 source_col_start: source_line_base_col + dedent_prefix_chars,
201 virtual_col_start: 4,
202 }));
203 }
204
205 let position_map = PositionMap {
206 line_mapping,
207 body_start_offset: def.body_span.start,
208 header_lines: 1,
209 };
210
211 let extension = normalize_file_extension(file_extension);
212 let virtual_path = workspace_dir.join(".shape-vdocs").join(format!(
213 "{}_{}.{}",
214 sanitize_filename(source_uri),
215 def.name,
216 extension
217 ));
218
219 VirtualDocument {
220 content: virtual_doc,
221 position_map,
222 language: def.language.clone(),
223 function_name: def.name.clone(),
224 source_uri: source_uri.to_string(),
225 virtual_path,
226 }
227}
228
229fn dedented_prefix_chars(raw_line: &str, dedented_line: &str) -> u32 {
230 if raw_line == dedented_line {
231 return 0;
232 }
233 if dedented_line.is_empty() {
234 return raw_line.chars().count() as u32;
235 }
236 for (byte_idx, _) in raw_line.char_indices() {
237 if raw_line[byte_idx..] == *dedented_line {
238 return raw_line[..byte_idx].chars().count() as u32;
239 }
240 }
241 0
242}
243
244fn normalize_file_extension(file_extension: &str) -> String {
245 let trimmed = file_extension.trim();
246 if trimmed.is_empty() {
247 return "txt".to_string();
248 }
249 trimmed.trim_start_matches('.').to_string()
250}
251
252struct ChildServer {
258 _process: Child,
259 stdin: tokio::process::ChildStdin,
261 pending: HashMap<u64, tokio::sync::oneshot::Sender<serde_json::Value>>,
263 next_id: u64,
265 initialized: bool,
267 semantic_token_types: Vec<String>,
269 semantic_token_modifiers: Vec<String>,
271 open_documents: HashMap<String, i32>,
273}
274
275#[derive(Clone, Debug)]
276struct RuntimeLspConfig {
277 server_command: Vec<String>,
278 file_extension: String,
279 extra_paths: Vec<String>,
280}
281
282#[derive(Clone, Debug)]
283struct VirtualUriMapping {
284 source_uri: Uri,
285 position_map: PositionMap,
286}
287
288#[derive(Clone, Debug)]
289struct ResolvedVirtualDocRequest {
290 language: String,
291 vdoc_uri: String,
292 virtual_pos: Position,
293 position_map: PositionMap,
294}
295
296#[derive(Clone, Copy, Debug, PartialEq, Eq)]
298pub struct ForeignSemanticToken {
299 pub line: u32,
300 pub start_char: u32,
301 pub length: u32,
302 pub token_type: u32,
303 pub token_modifiers_bitset: u32,
304}
305
306#[derive(Clone, Debug)]
308pub struct ConfiguredExtensionSpec {
309 pub name: String,
310 pub path: PathBuf,
311 pub config: serde_json::Value,
312}
313
314pub struct ForeignLspManager {
319 servers: Arc<Mutex<HashMap<String, ChildServer>>>,
321 documents: Arc<Mutex<HashMap<(String, String), VirtualDocument>>>,
323 published_diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
325 runtime_configs: Arc<Mutex<HashMap<String, RuntimeLspConfig>>>,
327 extension_registry: shape_runtime::provider_registry::ProviderRegistry,
329 loaded_extension_keys: Arc<Mutex<HashSet<String>>>,
331 configured_extensions: Arc<Mutex<Vec<ConfiguredExtensionSpec>>>,
333 workspace_dir: std::sync::RwLock<PathBuf>,
336}
337
338impl ForeignLspManager {
339 pub fn new(workspace_dir: PathBuf) -> Self {
340 Self {
341 servers: Arc::new(Mutex::new(HashMap::new())),
342 documents: Arc::new(Mutex::new(HashMap::new())),
343 published_diagnostics: Arc::new(Mutex::new(HashMap::new())),
344 runtime_configs: Arc::new(Mutex::new(HashMap::new())),
345 extension_registry: shape_runtime::provider_registry::ProviderRegistry::new(),
346 loaded_extension_keys: Arc::new(Mutex::new(HashSet::new())),
347 configured_extensions: Arc::new(Mutex::new(Vec::new())),
348 workspace_dir: std::sync::RwLock::new(workspace_dir),
349 }
350 }
351
352 pub fn set_workspace_dir(&self, dir: PathBuf) {
359 *self.workspace_dir.write().unwrap() = dir;
360 }
361
362 pub async fn set_configured_extensions(&self, specs: Vec<ConfiguredExtensionSpec>) {
364 let mut configured = self.configured_extensions.lock().await;
365 *configured = specs;
366 }
367
368 fn resolve_configured_extension_path(
369 path: &Path,
370 current_file: Option<&Path>,
371 workspace_root: Option<&Path>,
372 ) -> PathBuf {
373 if path.is_absolute() {
374 return path.to_path_buf();
375 }
376 if let Some(root) = workspace_root {
377 return root.join(path);
378 }
379 if let Some(file) = current_file
380 && let Some(parent) = file.parent()
381 {
382 return parent.join(path);
383 }
384 std::env::current_dir()
385 .unwrap_or_else(|_| PathBuf::from("."))
386 .join(path)
387 }
388
389 async fn refresh_runtime_configs(
391 &self,
392 current_file: Option<&Path>,
393 workspace_root: Option<&Path>,
394 source: Option<&str>,
395 ) {
396 let specs = shape_runtime::extension_context::declared_extension_specs_for_context(
397 current_file,
398 workspace_root,
399 source,
400 );
401 let configured_specs = self.configured_extensions.lock().await.clone();
402
403 {
404 let mut loaded_keys = self.loaded_extension_keys.lock().await;
405 for spec in specs {
406 let canonical = spec
407 .path
408 .canonicalize()
409 .unwrap_or_else(|_| spec.path.clone());
410 let config_key = serde_json::to_string(&spec.config).unwrap_or_default();
411 let identity = format!("{}|{}", canonical.to_string_lossy(), config_key);
412 if loaded_keys.contains(&identity) {
413 continue;
414 }
415
416 match self
417 .extension_registry
418 .load_extension(&spec.path, &spec.config)
419 {
420 Ok(_) => {
421 loaded_keys.insert(identity);
422 }
423 Err(err) => {
424 tracing::warn!(
425 "failed to load extension '{}' for foreign LSP config discovery: {}",
426 spec.name,
427 err
428 );
429 }
430 }
431 }
432
433 for spec in configured_specs {
434 let resolved_path = Self::resolve_configured_extension_path(
435 &spec.path,
436 current_file,
437 workspace_root,
438 );
439 let canonical = resolved_path
440 .canonicalize()
441 .unwrap_or_else(|_| resolved_path.clone());
442 let config_key = serde_json::to_string(&spec.config).unwrap_or_default();
443 let identity = format!("{}|{}", canonical.to_string_lossy(), config_key);
444 if loaded_keys.contains(&identity) {
445 continue;
446 }
447
448 match self
449 .extension_registry
450 .load_extension(&resolved_path, &spec.config)
451 {
452 Ok(_) => {
453 loaded_keys.insert(identity);
454 }
455 Err(err) => {
456 tracing::warn!(
457 "failed to load configured extension '{}' for foreign LSP config discovery: {}",
458 spec.name,
459 err
460 );
461 }
462 }
463 }
464 }
465
466 let runtime_configs = self.extension_registry.language_runtime_lsp_configs();
467 let mut configs = self.runtime_configs.lock().await;
468 configs.clear();
469 for runtime in runtime_configs {
470 configs.insert(
471 runtime.language_id.clone(),
472 RuntimeLspConfig {
473 server_command: runtime.server_command,
474 file_extension: runtime.file_extension,
475 extra_paths: runtime.extra_paths,
476 },
477 );
478 }
479 }
480
481 async fn runtime_config_for_language(&self, language: &str) -> Option<RuntimeLspConfig> {
482 let configs = self.runtime_configs.lock().await;
483 configs.get(language).cloned()
484 }
485
486 pub async fn start_server(&self, language: &str) -> Result<(), String> {
488 let runtime_cfg = self
489 .runtime_config_for_language(language)
490 .await
491 .ok_or_else(|| format!("No language runtime LSP config for '{}'", language))?;
492 let (cmd, args) = runtime_cfg
493 .server_command
494 .split_first()
495 .ok_or_else(|| format!("Empty server command for '{}'", language))?;
496
497 let mut servers = self.servers.lock().await;
498 if servers.contains_key(language) {
499 return Ok(());
500 }
501
502 let mut child = Command::new(cmd)
503 .args(args)
504 .stdin(std::process::Stdio::piped())
505 .stdout(std::process::Stdio::piped())
506 .stderr(std::process::Stdio::piped())
507 .spawn()
508 .map_err(|e| format!("Failed to start {} for '{}': {}", cmd, language, e))?;
509
510 let stdin = child.stdin.take().ok_or("Failed to capture child stdin")?;
511 let stdout = child
512 .stdout
513 .take()
514 .ok_or("Failed to capture child stdout")?;
515 let stderr = child
516 .stderr
517 .take()
518 .ok_or("Failed to capture child stderr")?;
519
520 let server = ChildServer {
521 _process: child,
522 stdin,
523 pending: HashMap::new(),
524 next_id: 1,
525 initialized: false,
526 semantic_token_types: Vec::new(),
527 semantic_token_modifiers: Vec::new(),
528 open_documents: HashMap::new(),
529 };
530
531 servers.insert(language.to_string(), server);
532
533 let servers_ref = Arc::clone(&self.servers);
535 let diagnostics_ref = Arc::clone(&self.published_diagnostics);
536 let lang = language.to_string();
537 tokio::spawn(async move {
538 let mut reader = BufReader::new(stdout);
539 let mut header_buf = String::new();
540
541 loop {
542 header_buf.clear();
543 match reader.read_line(&mut header_buf).await {
544 Ok(0) => break, Err(_) => break,
546 Ok(_) => {}
547 }
548
549 let content_length = if header_buf.starts_with("Content-Length:") {
551 header_buf
552 .trim_start_matches("Content-Length:")
553 .trim()
554 .parse::<usize>()
555 .ok()
556 } else {
557 None
558 };
559
560 header_buf.clear();
562 if reader.read_line(&mut header_buf).await.is_err() {
563 break;
564 }
565
566 if let Some(len) = content_length {
567 let mut body = vec![0u8; len];
568 if tokio::io::AsyncReadExt::read_exact(&mut reader, &mut body)
569 .await
570 .is_err()
571 {
572 break;
573 }
574
575 if let Ok(msg) = serde_json::from_slice::<serde_json::Value>(&body) {
576 if let Some(method) = msg.get("method").and_then(|v| v.as_str()) {
577 if method == "textDocument/publishDiagnostics" {
578 if let Some(params) = msg.get("params")
579 && let Some(uri) = params.get("uri").and_then(|v| v.as_str())
580 {
581 let diagnostics = params
582 .get("diagnostics")
583 .cloned()
584 .and_then(|v| {
585 serde_json::from_value::<Vec<Diagnostic>>(v).ok()
586 })
587 .unwrap_or_default();
588 diagnostics_ref
589 .lock()
590 .await
591 .insert(uri.to_string(), diagnostics);
592 }
593 }
594 }
595
596 if let Some(id) = msg.get("id").and_then(|v| v.as_u64()) {
598 if msg.get("method").is_none() {
599 let mut servers = servers_ref.lock().await;
600 if let Some(server) = servers.get_mut(&lang) {
601 if let Some(sender) = server.pending.remove(&id) {
602 let _ = sender.send(msg);
603 }
604 }
605 }
606 }
607 }
608 }
609 }
610 });
611
612 let stderr_lang = language.to_string();
613 tokio::spawn(async move {
614 let mut reader = BufReader::new(stderr);
615 let mut line = String::new();
616 loop {
617 line.clear();
618 match reader.read_line(&mut line).await {
619 Ok(0) => break,
620 Ok(_) => {
621 let trimmed = line.trim_end_matches(['\r', '\n']);
622 if !trimmed.is_empty() {
623 tracing::info!("child-lsp[{stderr_lang}] stderr: {trimmed}");
624 }
625 }
626 Err(err) => {
627 tracing::warn!("child-lsp[{stderr_lang}] stderr read failed: {err}");
628 break;
629 }
630 }
631 }
632 });
633
634 Ok(())
635 }
636
637 async fn initialize_server(&self, language: &str) -> Result<(), String> {
639 let runtime_cfg = self
640 .runtime_config_for_language(language)
641 .await
642 .ok_or_else(|| format!("No language runtime LSP config for '{}'", language))?;
643
644 let workspace_uri = format!("file://{}", self.workspace_dir.read().unwrap().display());
645 let workspace_path = self.workspace_dir.read().unwrap().display().to_string();
646 let mut init_params = serde_json::json!({
647 "processId": std::process::id(),
648 "capabilities": child_client_capabilities(),
649 "rootUri": workspace_uri,
652 "rootPath": workspace_path,
653 "workspaceFolders": [
654 {
655 "uri": workspace_uri,
656 "name": workspace_path,
657 }
658 ],
659 });
660 if !runtime_cfg.extra_paths.is_empty() {
661 init_params["initializationOptions"] = serde_json::json!({
662 "extraPaths": runtime_cfg.extra_paths,
663 });
664 }
665
666 tracing::info!(
667 "child-lsp[{}] initialize rootUri={} rootPath={}",
668 language,
669 workspace_uri,
670 workspace_path,
671 );
672
673 let response = self
674 .send_request(language, "initialize", init_params)
675 .await?;
676
677 self.send_notification(language, "initialized", serde_json::json!({}))
679 .await?;
680
681 let (semantic_token_types, semantic_token_modifiers) =
682 extract_semantic_tokens_legend(&response);
683
684 let mut servers = self.servers.lock().await;
685 if let Some(server) = servers.get_mut(language) {
686 server.initialized = true;
687 server.semantic_token_types = semantic_token_types;
688 server.semantic_token_modifiers = semantic_token_modifiers;
689 }
690
691 Ok(())
692 }
693
694 async fn send_request(
696 &self,
697 language: &str,
698 method: &str,
699 params: serde_json::Value,
700 ) -> Result<serde_json::Value, String> {
701 let (tx, rx) = tokio::sync::oneshot::channel();
702
703 let request_id;
704 {
705 let mut servers = self.servers.lock().await;
706 let server = servers
707 .get_mut(language)
708 .ok_or_else(|| format!("No server for '{}'", language))?;
709
710 request_id = server.next_id;
711 server.next_id += 1;
712 server.pending.insert(request_id, tx);
713
714 let msg = serde_json::json!({
715 "jsonrpc": "2.0",
716 "id": request_id,
717 "method": method,
718 "params": params,
719 });
720
721 let body = serde_json::to_string(&msg).map_err(|e| e.to_string())?;
722 let header = format!("Content-Length: {}\r\n\r\n", body.len());
723
724 server
725 .stdin
726 .write_all(header.as_bytes())
727 .await
728 .map_err(|e| format!("Failed to write to child stdin: {}", e))?;
729 server
730 .stdin
731 .write_all(body.as_bytes())
732 .await
733 .map_err(|e| format!("Failed to write to child stdin: {}", e))?;
734 server
735 .stdin
736 .flush()
737 .await
738 .map_err(|e| format!("Failed to flush child stdin: {}", e))?;
739 }
740
741 let response = rx
742 .await
743 .map_err(|_| "Child server response channel closed".to_string())?;
744 if let Some(error) = response.get("error") {
745 return Err(format!(
746 "Child server '{}' request '{}' failed: {}",
747 language, method, error
748 ));
749 }
750 Ok(response)
751 }
752
753 async fn send_notification(
755 &self,
756 language: &str,
757 method: &str,
758 params: serde_json::Value,
759 ) -> Result<(), String> {
760 let mut servers = self.servers.lock().await;
761 let server = servers
762 .get_mut(language)
763 .ok_or_else(|| format!("No server for '{}'", language))?;
764
765 let msg = serde_json::json!({
766 "jsonrpc": "2.0",
767 "method": method,
768 "params": params,
769 });
770
771 let body = serde_json::to_string(&msg).map_err(|e| e.to_string())?;
772 let header = format!("Content-Length: {}\r\n\r\n", body.len());
773
774 server
775 .stdin
776 .write_all(header.as_bytes())
777 .await
778 .map_err(|e| e.to_string())?;
779 server
780 .stdin
781 .write_all(body.as_bytes())
782 .await
783 .map_err(|e| e.to_string())?;
784 server.stdin.flush().await.map_err(|e| e.to_string())?;
785
786 Ok(())
787 }
788
789 async fn ensure_server_ready(&self, language: &str) -> Result<(), String> {
790 let (is_running, is_initialized) = {
791 let servers = self.servers.lock().await;
792 (
793 servers.contains_key(language),
794 servers
795 .get(language)
796 .map(|server| server.initialized)
797 .unwrap_or(false),
798 )
799 };
800
801 if !is_running {
802 self.start_server(language).await?;
803 }
804 if !is_initialized {
805 self.initialize_server(language).await?;
806 }
807 Ok(())
808 }
809
810 async fn close_virtual_document(&self, language: &str, vdoc_uri: &str) -> Result<(), String> {
811 let should_close = {
812 let servers = self.servers.lock().await;
813 servers
814 .get(language)
815 .map(|server| server.open_documents.contains_key(vdoc_uri))
816 .unwrap_or(false)
817 };
818
819 if !should_close {
820 return Ok(());
821 }
822
823 self.send_notification(
824 language,
825 "textDocument/didClose",
826 serde_json::json!({
827 "textDocument": { "uri": vdoc_uri }
828 }),
829 )
830 .await?;
831
832 let mut servers = self.servers.lock().await;
833 if let Some(server) = servers.get_mut(language) {
834 server.open_documents.remove(vdoc_uri);
835 }
836 Ok(())
837 }
838
839 async fn sync_virtual_document(
840 &self,
841 language: &str,
842 vdoc_uri: &str,
843 content: &str,
844 ) -> Result<(), String> {
845 let previous_version = {
846 let servers = self.servers.lock().await;
847 servers
848 .get(language)
849 .and_then(|server| server.open_documents.get(vdoc_uri).copied())
850 .unwrap_or(0)
851 };
852 let next_version = if previous_version > 0 {
853 previous_version + 1
854 } else {
855 1
856 };
857
858 if previous_version > 0 {
859 self.send_notification(
860 language,
861 "textDocument/didChange",
862 serde_json::json!({
863 "textDocument": {
864 "uri": vdoc_uri,
865 "version": next_version,
866 },
867 "contentChanges": [
868 { "text": content }
869 ],
870 }),
871 )
872 .await?;
873 } else {
874 self.send_notification(
875 language,
876 "textDocument/didOpen",
877 serde_json::json!({
878 "textDocument": {
879 "uri": vdoc_uri,
880 "languageId": language,
881 "version": next_version,
882 "text": content,
883 }
884 }),
885 )
886 .await?;
887 }
888
889 let mut servers = self.servers.lock().await;
890 if let Some(server) = servers.get_mut(language) {
891 server
892 .open_documents
893 .insert(vdoc_uri.to_string(), next_version);
894 }
895 Ok(())
896 }
897
898 pub async fn update_documents(
902 &self,
903 source_uri: &str,
904 source: &str,
905 items: &[Item],
906 current_file: Option<&Path>,
907 workspace_root: Option<&Path>,
908 ) -> Vec<Diagnostic> {
909 self.refresh_runtime_configs(current_file, workspace_root, Some(source))
910 .await;
911
912 let runtime_configs = self.runtime_configs.lock().await.clone();
913 let mut docs = self.documents.lock().await;
914 let mut language_ranges: HashMap<String, Range> = HashMap::new();
915 let mut missing_runtime_languages: HashSet<String> = HashSet::new();
916
917 let removed_virtual_docs: Vec<(String, String)> = docs
918 .iter()
919 .filter(|((uri, _), _)| uri == source_uri)
920 .map(|((_, _), doc)| {
921 (
922 doc.language.clone(),
923 format!("file://{}", doc.virtual_path.display()),
924 )
925 })
926 .collect();
927
928 docs.retain(|(uri, _), _| uri != source_uri);
930
931 for item in items {
932 if let Item::ForeignFunction(def, _) = item {
933 language_ranges
934 .entry(def.language.clone())
935 .or_insert_with(|| span_to_range(source, def.name_span));
936 let Some(runtime_cfg) = runtime_configs.get(&def.language) else {
937 missing_runtime_languages.insert(def.language.clone());
938 continue;
939 };
940
941 let vdoc = generate_virtual_document(
942 def,
943 source,
944 source_uri,
945 &self.workspace_dir.read().unwrap(),
946 &runtime_cfg.file_extension,
947 );
948
949 docs.insert((source_uri.to_string(), def.name.clone()), vdoc);
950 }
951 }
952
953 for ((uri, _fn_name), vdoc) in docs.iter() {
955 if uri != source_uri {
956 continue;
957 }
958 if let Some(parent) = vdoc.virtual_path.parent() {
960 let _ = std::fs::create_dir_all(parent);
961 }
962 let _ = std::fs::write(&vdoc.virtual_path, &vdoc.content);
963 }
964
965 let docs_snapshot: Vec<(String, String, String)> = docs
967 .iter()
968 .filter(|((uri, _), _)| uri == source_uri)
969 .map(|((_, _fn_name), vdoc)| {
970 (
971 vdoc.language.clone(),
972 format!("file://{}", vdoc.virtual_path.display()),
973 vdoc.content.clone(),
974 )
975 })
976 .collect();
977 drop(docs);
978
979 if !removed_virtual_docs.is_empty() {
980 let mut published = self.published_diagnostics.lock().await;
981 for (_, uri) in &removed_virtual_docs {
982 published.remove(uri.as_str());
983 }
984 }
985
986 let mut startup_diagnostics = Vec::new();
987 let mut seen_issue_keys = HashSet::new();
988
989 let mut push_issue = |language: &str, message: String| {
990 let key = format!("{language}|{message}");
991 if !seen_issue_keys.insert(key) {
992 return;
993 }
994 let range = language_ranges
995 .get(language)
996 .cloned()
997 .unwrap_or_else(fallback_range);
998 startup_diagnostics.push(Diagnostic {
999 range,
1000 severity: Some(DiagnosticSeverity::WARNING),
1001 source: Some("shape-foreign".to_string()),
1002 message,
1003 ..Default::default()
1004 });
1005 };
1006
1007 for language in missing_runtime_languages {
1008 push_issue(
1009 &language,
1010 format!(
1011 "No child LSP config for foreign language '{}' (load the matching extension).",
1012 language
1013 ),
1014 );
1015 }
1016
1017 for (language, vdoc_uri) in removed_virtual_docs {
1018 if let Err(err) = self.close_virtual_document(&language, &vdoc_uri).await {
1019 push_issue(
1020 &language,
1021 format!("Failed to close foreign virtual document: {}", err),
1022 );
1023 }
1024 }
1025
1026 for (language, vdoc_uri, content) in docs_snapshot {
1028 if let Err(err) = self.ensure_server_ready(&language).await {
1029 push_issue(
1030 &language,
1031 format!(
1032 "Failed to start/initialize child LSP '{}': {}",
1033 language, err
1034 ),
1035 );
1036 continue;
1037 }
1038
1039 if let Err(err) = self
1040 .sync_virtual_document(&language, &vdoc_uri, &content)
1041 .await
1042 {
1043 push_issue(
1044 &language,
1045 format!("Failed to sync foreign virtual document: {}", err),
1046 );
1047 }
1048 }
1049
1050 startup_diagnostics
1051 }
1052
1053 async fn resolve_virtual_doc_request(
1054 &self,
1055 source_uri: &str,
1056 position: Position,
1057 items: &[Item],
1058 source: &str,
1059 ) -> Option<ResolvedVirtualDocRequest> {
1060 let offset = crate::util::position_to_offset(source, position)?;
1061 let def = find_foreign_block_at_offset(items, offset)?;
1062 let docs = self.documents.lock().await;
1063 let vdoc = docs.get(&(source_uri.to_string(), def.name.clone()))?;
1064 let virtual_pos = vdoc.position_map.shape_to_virtual(position)?;
1065 Some(ResolvedVirtualDocRequest {
1066 language: vdoc.language.clone(),
1067 vdoc_uri: format!("file://{}", vdoc.virtual_path.display()),
1068 virtual_pos,
1069 position_map: vdoc.position_map.clone(),
1070 })
1071 }
1072
1073 async fn virtual_uri_mappings(&self) -> HashMap<String, VirtualUriMapping> {
1074 let docs = self.documents.lock().await;
1075 docs.iter()
1076 .filter_map(|((_, _), vdoc)| {
1077 let source_uri = Uri::from_str(&vdoc.source_uri).ok()?;
1078 Some((
1079 format!("file://{}", vdoc.virtual_path.display()),
1080 VirtualUriMapping {
1081 source_uri,
1082 position_map: vdoc.position_map.clone(),
1083 },
1084 ))
1085 })
1086 .collect()
1087 }
1088
1089 pub async fn handle_completion(
1093 &self,
1094 source_uri: &str,
1095 position: Position,
1096 items: &[Item],
1097 source: &str,
1098 ) -> Option<Vec<CompletionItem>> {
1099 let resolved = self
1100 .resolve_virtual_doc_request(source_uri, position, items, source)
1101 .await?;
1102
1103 let params = serde_json::json!({
1104 "textDocument": { "uri": resolved.vdoc_uri },
1105 "position": { "line": resolved.virtual_pos.line, "character": resolved.virtual_pos.character },
1106 });
1107
1108 let response = self
1109 .send_request(&resolved.language, "textDocument/completion", params)
1110 .await
1111 .ok()?;
1112
1113 parse_completion_response(response)
1115 }
1116
1117 pub async fn handle_hover(
1119 &self,
1120 source_uri: &str,
1121 position: Position,
1122 items: &[Item],
1123 source: &str,
1124 ) -> Option<Hover> {
1125 let resolved = self
1126 .resolve_virtual_doc_request(source_uri, position, items, source)
1127 .await?;
1128
1129 let params = serde_json::json!({
1130 "textDocument": { "uri": resolved.vdoc_uri },
1131 "position": { "line": resolved.virtual_pos.line, "character": resolved.virtual_pos.character },
1132 });
1133
1134 let response = self
1135 .send_request(&resolved.language, "textDocument/hover", params)
1136 .await
1137 .ok()?;
1138
1139 let mut hover = parse_hover_response(response)?;
1140 if let Some(range) = hover.range {
1141 hover.range = resolved.position_map.virtual_range_to_shape(range);
1142 }
1143 Some(hover)
1144 }
1145
1146 pub async fn handle_signature_help(
1148 &self,
1149 source_uri: &str,
1150 position: Position,
1151 items: &[Item],
1152 source: &str,
1153 ) -> Option<SignatureHelp> {
1154 let resolved = self
1155 .resolve_virtual_doc_request(source_uri, position, items, source)
1156 .await?;
1157
1158 let params = serde_json::json!({
1159 "textDocument": { "uri": resolved.vdoc_uri },
1160 "position": { "line": resolved.virtual_pos.line, "character": resolved.virtual_pos.character },
1161 });
1162
1163 let response = self
1164 .send_request(&resolved.language, "textDocument/signatureHelp", params)
1165 .await
1166 .ok()?;
1167 parse_signature_help_response(response)
1168 }
1169
1170 pub async fn handle_definition(
1172 &self,
1173 source_uri: &str,
1174 position: Position,
1175 items: &[Item],
1176 source: &str,
1177 ) -> Option<GotoDefinitionResponse> {
1178 let resolved = self
1179 .resolve_virtual_doc_request(source_uri, position, items, source)
1180 .await?;
1181
1182 let params = serde_json::json!({
1183 "textDocument": { "uri": resolved.vdoc_uri },
1184 "position": { "line": resolved.virtual_pos.line, "character": resolved.virtual_pos.character },
1185 });
1186
1187 let response = self
1188 .send_request(&resolved.language, "textDocument/definition", params)
1189 .await
1190 .ok()?;
1191 let definition = parse_definition_response(response)?;
1192 let mappings = self.virtual_uri_mappings().await;
1193 Some(map_definition_response_to_shape(definition, &mappings))
1194 }
1195
1196 pub async fn handle_references(
1198 &self,
1199 source_uri: &str,
1200 position: Position,
1201 items: &[Item],
1202 source: &str,
1203 ) -> Option<Vec<Location>> {
1204 let resolved = self
1205 .resolve_virtual_doc_request(source_uri, position, items, source)
1206 .await?;
1207
1208 let params = serde_json::json!({
1209 "textDocument": { "uri": resolved.vdoc_uri },
1210 "position": { "line": resolved.virtual_pos.line, "character": resolved.virtual_pos.character },
1211 "context": { "includeDeclaration": true },
1212 });
1213
1214 let response = self
1215 .send_request(&resolved.language, "textDocument/references", params)
1216 .await
1217 .ok()?;
1218 let references = parse_references_response(response)?;
1219 let mappings = self.virtual_uri_mappings().await;
1220 Some(
1221 references
1222 .into_iter()
1223 .map(|location| map_location_to_shape(location, &mappings))
1224 .collect(),
1225 )
1226 }
1227
1228 pub async fn collect_semantic_tokens(&self, source_uri: &str) -> Vec<ForeignSemanticToken> {
1230 let docs_snapshot: Vec<(String, String, PositionMap, String)> = {
1231 let docs = self.documents.lock().await;
1232 docs.iter()
1233 .filter(|((uri, _), _)| uri == source_uri)
1234 .map(|((_, _), vdoc)| {
1235 (
1236 vdoc.language.clone(),
1237 format!("file://{}", vdoc.virtual_path.display()),
1238 vdoc.position_map.clone(),
1239 vdoc.content.clone(),
1240 )
1241 })
1242 .collect()
1243 };
1244
1245 let mut collected = Vec::new();
1246 for (language, vdoc_uri, position_map, content) in docs_snapshot {
1247 let (mut token_types, mut token_modifiers) = {
1248 let servers = self.servers.lock().await;
1249 let Some(server) = servers.get(&language) else {
1250 continue;
1251 };
1252 (
1253 server.semantic_token_types.clone(),
1254 server.semantic_token_modifiers.clone(),
1255 )
1256 };
1257 if token_types.is_empty() {
1258 token_types = CHILD_SEMANTIC_TOKEN_TYPES
1259 .iter()
1260 .map(|s| (*s).to_string())
1261 .collect();
1262 }
1263 if token_modifiers.is_empty() {
1264 token_modifiers = CHILD_SEMANTIC_TOKEN_MODIFIERS
1265 .iter()
1266 .map(|s| (*s).to_string())
1267 .collect();
1268 }
1269
1270 let mut tokens = match self
1271 .request_semantic_tokens(&language, &vdoc_uri, &content)
1272 .await
1273 {
1274 Ok(tokens) => tokens,
1275 Err(err) => {
1276 tracing::info!(
1277 "child-lsp[{language}] semantic tokens unavailable for {}: {err}",
1278 vdoc_uri
1279 );
1280 continue;
1281 }
1282 };
1283
1284 if tokens
1287 .as_ref()
1288 .is_none_or(|token_set| token_set.data.is_empty())
1289 {
1290 tokio::time::sleep(Duration::from_millis(40)).await;
1291 if let Ok(retry_tokens) = self
1292 .request_semantic_tokens(&language, &vdoc_uri, &content)
1293 .await
1294 && retry_tokens
1295 .as_ref()
1296 .is_some_and(|token_set| !token_set.data.is_empty())
1297 {
1298 tokens = retry_tokens;
1299 }
1300 }
1301
1302 let Some(tokens) = tokens else {
1303 continue;
1304 };
1305
1306 collected.extend(map_foreign_semantic_tokens_to_shape(
1307 &tokens,
1308 &token_types,
1309 &token_modifiers,
1310 &position_map,
1311 ));
1312 }
1313
1314 collected
1315 }
1316
1317 async fn request_semantic_tokens(
1318 &self,
1319 language: &str,
1320 vdoc_uri: &str,
1321 content: &str,
1322 ) -> Result<Option<SemanticTokens>, String> {
1323 let full_params = serde_json::json!({
1324 "textDocument": { "uri": vdoc_uri },
1325 });
1326 let mut full_error: Option<String> = None;
1327
1328 match self
1329 .send_request(language, "textDocument/semanticTokens/full", full_params)
1330 .await
1331 {
1332 Ok(response) => {
1333 if let Some(tokens) = parse_semantic_tokens_response(response) {
1334 return Ok(Some(tokens));
1335 }
1336 }
1337 Err(err) => {
1338 full_error = Some(err);
1339 }
1340 }
1341
1342 let legacy_params = serde_json::json!({
1345 "textDocument": { "uri": vdoc_uri },
1346 });
1347 let mut legacy_error: Option<String> = None;
1348 match self
1349 .send_request(language, "textDocument/semanticTokens", legacy_params)
1350 .await
1351 {
1352 Ok(response) => {
1353 if let Some(tokens) = parse_semantic_tokens_response(response) {
1354 return Ok(Some(tokens));
1355 }
1356 }
1357 Err(err) => {
1358 legacy_error = Some(err);
1359 }
1360 }
1361
1362 let range = full_document_semantic_tokens_range(content);
1363 let range_params = serde_json::json!({
1364 "textDocument": { "uri": vdoc_uri },
1365 "range": {
1366 "start": { "line": range.start.line, "character": range.start.character },
1367 "end": { "line": range.end.line, "character": range.end.character },
1368 },
1369 });
1370
1371 match self
1372 .send_request(language, "textDocument/semanticTokens/range", range_params)
1373 .await
1374 {
1375 Ok(response) => Ok(parse_semantic_tokens_response(response)),
1376 Err(range_err) => {
1377 if let (Some(full_err), Some(legacy_err)) =
1378 (full_error.as_ref(), legacy_error.as_ref())
1379 {
1380 Err(format!(
1381 "full request failed ({full_err}); legacy request failed ({legacy_err}); range request failed ({range_err})"
1382 ))
1383 } else if let Some(full_err) = full_error.as_ref() {
1384 Err(format!(
1385 "full request failed ({full_err}); range request failed ({range_err})"
1386 ))
1387 } else if let Some(legacy_err) = legacy_error.as_ref() {
1388 Err(format!(
1389 "legacy request failed ({legacy_err}); range request failed ({range_err})"
1390 ))
1391 } else {
1392 Err(format!("range request failed ({range_err})"))
1393 }
1394 }
1395 }
1396 }
1397
1398 pub async fn get_diagnostics(&self, source_uri: &str) -> Vec<Diagnostic> {
1402 let docs_snapshot: Vec<(String, String, PositionMap)> = {
1403 let docs = self.documents.lock().await;
1404 docs.iter()
1405 .filter(|((uri, _), _)| uri == source_uri)
1406 .map(|((_, _), vdoc)| {
1407 (
1408 vdoc.language.clone(),
1409 format!("file://{}", vdoc.virtual_path.display()),
1410 vdoc.position_map.clone(),
1411 )
1412 })
1413 .collect()
1414 };
1415
1416 let mut all_diagnostics = Vec::new();
1417
1418 for (language, vdoc_uri, position_map) in docs_snapshot {
1419 let mut diagnostics = None;
1420
1421 let request_params = serde_json::json!({
1422 "textDocument": { "uri": vdoc_uri },
1423 "identifier": "shape-foreign",
1424 });
1425 if let Ok(response) = self
1426 .send_request(&language, "textDocument/diagnostic", request_params)
1427 .await
1428 {
1429 diagnostics = parse_document_diagnostic_report(response);
1430 }
1431
1432 if diagnostics.is_none() {
1433 let published = self.published_diagnostics.lock().await;
1434 diagnostics = published.get(&vdoc_uri).cloned();
1435 }
1436
1437 let Some(diagnostics) = diagnostics else {
1438 continue;
1439 };
1440 for diagnostic in &diagnostics {
1441 if let Some(mapped) = Self::map_diagnostic_to_shape(diagnostic, &position_map) {
1442 all_diagnostics.push(mapped);
1443 }
1444 }
1445 }
1446
1447 all_diagnostics
1448 }
1449
1450 pub fn map_diagnostic_to_shape(
1452 diagnostic: &Diagnostic,
1453 position_map: &PositionMap,
1454 ) -> Option<Diagnostic> {
1455 let range = position_map.virtual_range_to_shape(diagnostic.range)?;
1456 Some(Diagnostic {
1457 range,
1458 severity: diagnostic.severity,
1459 code: diagnostic.code.clone(),
1460 code_description: diagnostic.code_description.clone(),
1461 source: diagnostic.source.clone(),
1462 message: diagnostic.message.clone(),
1463 related_information: None,
1464 tags: diagnostic.tags.clone(),
1465 data: diagnostic.data.clone(),
1466 })
1467 }
1468
1469 pub async fn shutdown(&self) {
1471 let mut servers = self.servers.lock().await;
1472 for (language, server) in servers.iter_mut() {
1473 let msg = serde_json::json!({
1475 "jsonrpc": "2.0",
1476 "id": server.next_id,
1477 "method": "shutdown",
1478 "params": null,
1479 });
1480 server.next_id += 1;
1481
1482 if let Ok(body) = serde_json::to_string(&msg) {
1483 let header = format!("Content-Length: {}\r\n\r\n", body.len());
1484 let _ = server.stdin.write_all(header.as_bytes()).await;
1485 let _ = server.stdin.write_all(body.as_bytes()).await;
1486 let _ = server.stdin.flush().await;
1487 }
1488
1489 let exit_msg = serde_json::json!({
1491 "jsonrpc": "2.0",
1492 "method": "exit",
1493 "params": null,
1494 });
1495 if let Ok(body) = serde_json::to_string(&exit_msg) {
1496 let header = format!("Content-Length: {}\r\n\r\n", body.len());
1497 let _ = server.stdin.write_all(header.as_bytes()).await;
1498 let _ = server.stdin.write_all(body.as_bytes()).await;
1499 let _ = server.stdin.flush().await;
1500 }
1501
1502 tracing::info!("Shut down child LSP for '{}'", language);
1503 }
1504 servers.clear();
1505 }
1506}
1507
1508pub fn find_foreign_block_at_offset(items: &[Item], offset: usize) -> Option<&ForeignFunctionDef> {
1514 for item in items {
1515 if let Item::ForeignFunction(def, _) = item {
1516 if offset >= def.body_span.start && offset < def.body_span.end {
1517 return Some(def);
1518 }
1519 }
1520 }
1521 None
1522}
1523
1524pub fn is_position_in_foreign_block(items: &[Item], source: &str, position: Position) -> bool {
1526 if let Some(offset) = crate::util::position_to_offset(source, position) {
1527 find_foreign_block_at_offset(items, offset).is_some()
1528 } else {
1529 false
1530 }
1531}
1532
1533fn parse_completion_response(response: serde_json::Value) -> Option<Vec<CompletionItem>> {
1538 let result = response.get("result")?;
1539
1540 let items_val = if let Some(items) = result.get("items") {
1542 items
1543 } else if result.is_array() {
1544 result
1545 } else {
1546 return None;
1547 };
1548
1549 let arr = items_val.as_array()?;
1550 let mut completions = Vec::new();
1551
1552 for item in arr {
1553 let label = item.get("label")?.as_str()?.to_string();
1554 let kind = item
1555 .get("kind")
1556 .and_then(|k| k.as_u64())
1557 .and_then(map_completion_item_kind);
1558 let detail = item
1559 .get("detail")
1560 .and_then(|d| d.as_str())
1561 .map(String::from);
1562 let documentation = item.get("documentation").and_then(|d| {
1563 if let Some(s) = d.as_str() {
1564 Some(tower_lsp_server::ls_types::Documentation::String(
1565 s.to_string(),
1566 ))
1567 } else {
1568 None
1569 }
1570 });
1571
1572 completions.push(CompletionItem {
1573 label,
1574 kind,
1575 detail,
1576 documentation,
1577 ..Default::default()
1578 });
1579 }
1580
1581 Some(completions)
1582}
1583
1584fn map_completion_item_kind(kind: u64) -> Option<CompletionItemKind> {
1585 match kind {
1587 1 => Some(CompletionItemKind::TEXT),
1588 2 => Some(CompletionItemKind::METHOD),
1589 3 => Some(CompletionItemKind::FUNCTION),
1590 4 => Some(CompletionItemKind::CONSTRUCTOR),
1591 5 => Some(CompletionItemKind::FIELD),
1592 6 => Some(CompletionItemKind::VARIABLE),
1593 7 => Some(CompletionItemKind::CLASS),
1594 8 => Some(CompletionItemKind::INTERFACE),
1595 9 => Some(CompletionItemKind::MODULE),
1596 10 => Some(CompletionItemKind::PROPERTY),
1597 _ => None,
1598 }
1599}
1600
1601fn parse_hover_response(response: serde_json::Value) -> Option<Hover> {
1602 let result = response.get("result")?;
1603 if result.is_null() {
1604 return None;
1605 }
1606 serde_json::from_value(result.clone()).ok()
1607}
1608
1609fn parse_signature_help_response(response: serde_json::Value) -> Option<SignatureHelp> {
1610 let result = response.get("result")?;
1611 if result.is_null() {
1612 return None;
1613 }
1614 serde_json::from_value(result.clone()).ok()
1615}
1616
1617fn parse_definition_response(response: serde_json::Value) -> Option<GotoDefinitionResponse> {
1618 let result = response.get("result")?;
1619 if result.is_null() {
1620 return None;
1621 }
1622 serde_json::from_value(result.clone()).ok()
1623}
1624
1625fn parse_references_response(response: serde_json::Value) -> Option<Vec<Location>> {
1626 let result = response.get("result")?;
1627 if result.is_null() {
1628 return Some(Vec::new());
1629 }
1630 serde_json::from_value(result.clone()).ok()
1631}
1632
1633fn parse_document_diagnostic_report(response: serde_json::Value) -> Option<Vec<Diagnostic>> {
1634 let result = response.get("result")?;
1635 if result.is_null() {
1636 return None;
1637 }
1638
1639 let items = result
1640 .get("items")
1641 .and_then(|v| v.as_array())
1642 .cloned()
1643 .or_else(|| {
1644 result
1645 .pointer("/fullDocumentDiagnosticReport/items")
1646 .and_then(|v| v.as_array())
1647 .cloned()
1648 })?;
1649
1650 let mut diagnostics = Vec::with_capacity(items.len());
1651 for item in items {
1652 let Ok(parsed) = serde_json::from_value::<Diagnostic>(item) else {
1653 continue;
1654 };
1655 diagnostics.push(parsed);
1656 }
1657 Some(diagnostics)
1658}
1659
1660fn parse_semantic_tokens_response(response: serde_json::Value) -> Option<SemanticTokens> {
1661 let result = response.get("result")?;
1662 if result.is_null() {
1663 return None;
1664 }
1665 serde_json::from_value(result.clone()).ok()
1666}
1667
1668fn extract_semantic_tokens_legend(
1669 initialize_response: &serde_json::Value,
1670) -> (Vec<String>, Vec<String>) {
1671 let Some(provider) = initialize_response.pointer("/result/capabilities/semanticTokensProvider")
1672 else {
1673 return (Vec::new(), Vec::new());
1674 };
1675 if provider.is_null() {
1676 return (Vec::new(), Vec::new());
1677 }
1678
1679 let legend = provider.get("legend").unwrap_or(provider);
1680
1681 let mut token_types: Vec<String> = legend
1682 .get("tokenTypes")
1683 .or_else(|| provider.get("tokenTypes"))
1684 .and_then(|v| v.as_array())
1685 .map(|arr| {
1686 arr.iter()
1687 .filter_map(|v| v.as_str().map(String::from))
1688 .collect()
1689 })
1690 .unwrap_or_default();
1691 let mut token_modifiers: Vec<String> = legend
1692 .get("tokenModifiers")
1693 .or_else(|| provider.get("tokenModifiers"))
1694 .and_then(|v| v.as_array())
1695 .map(|arr| {
1696 arr.iter()
1697 .filter_map(|v| v.as_str().map(String::from))
1698 .collect()
1699 })
1700 .unwrap_or_default();
1701
1702 if token_types.is_empty() {
1705 token_types = CHILD_SEMANTIC_TOKEN_TYPES
1706 .iter()
1707 .map(|s| (*s).to_string())
1708 .collect();
1709 }
1710 if token_modifiers.is_empty() {
1711 token_modifiers = CHILD_SEMANTIC_TOKEN_MODIFIERS
1712 .iter()
1713 .map(|s| (*s).to_string())
1714 .collect();
1715 }
1716
1717 (token_types, token_modifiers)
1718}
1719
1720const CHILD_SEMANTIC_TOKEN_TYPES: &[&str] = &[
1721 "namespace",
1722 "type",
1723 "class",
1724 "enum",
1725 "interface",
1726 "struct",
1727 "typeParameter",
1728 "parameter",
1729 "variable",
1730 "property",
1731 "enumMember",
1732 "event",
1733 "function",
1734 "method",
1735 "macro",
1736 "keyword",
1737 "modifier",
1738 "comment",
1739 "string",
1740 "number",
1741 "regexp",
1742 "operator",
1743 "decorator",
1744];
1745
1746const CHILD_SEMANTIC_TOKEN_MODIFIERS: &[&str] = &[
1747 "declaration",
1748 "definition",
1749 "readonly",
1750 "static",
1751 "deprecated",
1752 "abstract",
1753 "async",
1754 "modification",
1755 "documentation",
1756 "defaultLibrary",
1757];
1758
1759fn child_client_capabilities() -> serde_json::Value {
1760 serde_json::json!({
1761 "textDocument": {
1762 "hover": {
1763 "contentFormat": ["markdown", "plaintext"]
1764 },
1765 "completion": {
1766 "completionItem": {
1767 "documentationFormat": ["markdown", "plaintext"]
1768 }
1769 },
1770 "semanticTokens": {
1771 "dynamicRegistration": false,
1772 "requests": {
1773 "range": true,
1774 "full": true
1775 },
1776 "tokenTypes": CHILD_SEMANTIC_TOKEN_TYPES,
1777 "tokenModifiers": CHILD_SEMANTIC_TOKEN_MODIFIERS,
1778 "formats": ["relative"],
1779 "multilineTokenSupport": true,
1780 "overlappingTokenSupport": true,
1781 "augmentsSyntaxTokens": true
1782 }
1783 }
1784 })
1785}
1786
1787fn map_definition_response_to_shape(
1788 definition: GotoDefinitionResponse,
1789 mappings: &HashMap<String, VirtualUriMapping>,
1790) -> GotoDefinitionResponse {
1791 match definition {
1792 GotoDefinitionResponse::Scalar(location) => {
1793 GotoDefinitionResponse::Scalar(map_location_to_shape(location, mappings))
1794 }
1795 GotoDefinitionResponse::Array(locations) => GotoDefinitionResponse::Array(
1796 locations
1797 .into_iter()
1798 .map(|location| map_location_to_shape(location, mappings))
1799 .collect(),
1800 ),
1801 GotoDefinitionResponse::Link(links) => GotoDefinitionResponse::Link(
1802 links
1803 .into_iter()
1804 .map(|link| map_location_link_to_shape(link, mappings))
1805 .collect(),
1806 ),
1807 }
1808}
1809
1810fn map_location_to_shape(
1811 location: Location,
1812 mappings: &HashMap<String, VirtualUriMapping>,
1813) -> Location {
1814 let Some(mapping) = mappings.get(location.uri.as_str()) else {
1815 return location;
1816 };
1817 let Some(range) = mapping.position_map.virtual_range_to_shape(location.range) else {
1818 return location;
1819 };
1820 Location {
1821 uri: mapping.source_uri.clone(),
1822 range,
1823 }
1824}
1825
1826fn map_location_link_to_shape(
1827 link: LocationLink,
1828 mappings: &HashMap<String, VirtualUriMapping>,
1829) -> LocationLink {
1830 let Some(mapping) = mappings.get(link.target_uri.as_str()) else {
1831 return link;
1832 };
1833 let Some(target_range) = mapping
1834 .position_map
1835 .virtual_range_to_shape(link.target_range)
1836 else {
1837 return link;
1838 };
1839 let Some(target_selection_range) = mapping
1840 .position_map
1841 .virtual_range_to_shape(link.target_selection_range)
1842 else {
1843 return link;
1844 };
1845
1846 LocationLink {
1847 origin_selection_range: link.origin_selection_range,
1848 target_uri: mapping.source_uri.clone(),
1849 target_range,
1850 target_selection_range,
1851 }
1852}
1853
1854fn map_foreign_semantic_tokens_to_shape(
1855 child_tokens: &SemanticTokens,
1856 child_token_types: &[String],
1857 child_token_modifiers: &[String],
1858 position_map: &PositionMap,
1859) -> Vec<ForeignSemanticToken> {
1860 let mut out = Vec::new();
1861 let mut line = 0u32;
1862 let mut col = 0u32;
1863
1864 for token in &child_tokens.data {
1865 line += token.delta_line;
1866 if token.delta_line == 0 {
1867 col += token.delta_start;
1868 } else {
1869 col = token.delta_start;
1870 }
1871
1872 let Some(type_name) = child_token_types.get(token.token_type as usize) else {
1873 continue;
1874 };
1875 let Some(mapped_type) = map_semantic_token_type_name_to_shape_index(type_name) else {
1876 continue;
1877 };
1878
1879 let mapped_modifiers = map_semantic_token_modifier_bits_to_shape(
1880 token.token_modifiers_bitset,
1881 child_token_modifiers,
1882 );
1883 let Some(shape_pos) = position_map.virtual_to_shape(Position {
1884 line,
1885 character: col,
1886 }) else {
1887 continue;
1888 };
1889
1890 out.push(ForeignSemanticToken {
1891 line: shape_pos.line,
1892 start_char: shape_pos.character,
1893 length: token.length,
1894 token_type: mapped_type,
1895 token_modifiers_bitset: mapped_modifiers,
1896 });
1897 }
1898
1899 out
1900}
1901
1902fn map_semantic_token_type_name_to_shape_index(name: &str) -> Option<u32> {
1903 match name {
1904 "namespace" => Some(0),
1905 "type" | "typeParameter" | "builtinType" => Some(1),
1906 "class" => Some(2),
1907 "enum" => Some(3),
1908 "function" | "builtinFunction" => Some(4),
1909 "variable" | "builtinVariable" => Some(5),
1910 "parameter" | "selfParameter" | "clsParameter" => Some(6),
1911 "property" | "member" => Some(7),
1912 "keyword" => Some(8),
1913 "string" | "regexp" => Some(9),
1914 "number" | "boolean" => Some(10),
1915 "operator" => Some(11),
1916 "comment" => Some(12),
1917 "macro" => Some(13),
1918 "decorator" => Some(14),
1919 "interface" => Some(15),
1920 "enumMember" => Some(16),
1921 "method" => Some(17),
1922 _ => {
1923 let normalized = name
1924 .chars()
1925 .filter(|ch| ch.is_ascii_alphanumeric())
1926 .collect::<String>()
1927 .to_ascii_lowercase();
1928
1929 if normalized.contains("namespace") {
1930 return Some(0);
1931 }
1932 if normalized.contains("interface") {
1933 return Some(15);
1934 }
1935 if normalized.contains("enummember") {
1936 return Some(16);
1937 }
1938 if normalized.contains("enum") {
1939 return Some(3);
1940 }
1941 if normalized.contains("classmethod") || normalized.contains("method") {
1942 return Some(17);
1943 }
1944 if normalized.contains("function") || normalized.contains("callable") {
1945 return Some(4);
1946 }
1947 if normalized.contains("typeparam")
1948 || normalized.contains("builtintype")
1949 || normalized == "type"
1950 {
1951 return Some(1);
1952 }
1953 if normalized.contains("class") {
1954 return Some(2);
1955 }
1956 if normalized.contains("parameter") || normalized.ends_with("param") {
1957 return Some(6);
1958 }
1959 if normalized.contains("property") || normalized.contains("member") {
1960 return Some(7);
1961 }
1962 if normalized.contains("keyword") {
1963 return Some(8);
1964 }
1965 if normalized.contains("string") || normalized.contains("regexp") {
1966 return Some(9);
1967 }
1968 if normalized.contains("number") || normalized.contains("boolean") {
1969 return Some(10);
1970 }
1971 if normalized.contains("operator") {
1972 return Some(11);
1973 }
1974 if normalized.contains("comment") {
1975 return Some(12);
1976 }
1977 if normalized.contains("decorator") {
1978 return Some(14);
1979 }
1980 if normalized.contains("variable") || normalized.contains("builtin") {
1981 return Some(5);
1982 }
1983 None
1984 }
1985 }
1986}
1987
1988fn map_semantic_token_modifier_bits_to_shape(bits: u32, child_modifiers: &[String]) -> u32 {
1989 let mut mapped = 0u32;
1990 for (idx, name) in child_modifiers.iter().enumerate() {
1991 let bit = 1u32 << idx;
1992 if bits & bit == 0 {
1993 continue;
1994 }
1995 match name.as_str() {
1996 "declaration" => mapped |= 1, "definition" => mapped |= 1 << 1, "readonly" => mapped |= 1 << 2, "static" => mapped |= 1 << 3, "deprecated" => mapped |= 1 << 4, "defaultLibrary" | "defaultlibrary" | "builtin" => mapped |= 1 << 5, "modification" => mapped |= 1 << 6, _ => {}
2004 }
2005 }
2006 mapped
2007}
2008
2009fn full_document_semantic_tokens_range(content: &str) -> Range {
2010 if content.is_empty() {
2011 return Range {
2012 start: Position {
2013 line: 0,
2014 character: 0,
2015 },
2016 end: Position {
2017 line: 0,
2018 character: 0,
2019 },
2020 };
2021 }
2022
2023 let mut line = 0u32;
2024 let mut last_line_len = 0u32;
2025 for chunk in content.split('\n') {
2026 last_line_len = chunk.chars().count() as u32;
2027 line += 1;
2028 }
2029 let end_line = line.saturating_sub(1);
2030
2031 Range {
2032 start: Position {
2033 line: 0,
2034 character: 0,
2035 },
2036 end: Position {
2037 line: end_line,
2038 character: last_line_len,
2039 },
2040 }
2041}
2042
2043fn span_to_range(source: &str, span: Span) -> Range {
2048 let (start_line, start_col) = crate::util::offset_to_line_col(source, span.start);
2049 let (end_line, end_col) = crate::util::offset_to_line_col(source, span.end);
2050 Range {
2051 start: Position {
2052 line: start_line,
2053 character: start_col,
2054 },
2055 end: Position {
2056 line: end_line,
2057 character: end_col,
2058 },
2059 }
2060}
2061
2062fn fallback_range() -> Range {
2063 Range {
2064 start: Position {
2065 line: 0,
2066 character: 0,
2067 },
2068 end: Position {
2069 line: 0,
2070 character: 1,
2071 },
2072 }
2073}
2074
2075fn sanitize_filename(uri: &str) -> String {
2077 uri.replace("://", "_")
2078 .replace('/', "_")
2079 .replace('\\', "_")
2080 .replace(':', "_")
2081 .replace('.', "_")
2082}
2083
2084#[cfg(test)]
2089mod tests {
2090 use super::*;
2091 use tower_lsp_server::ls_types::{HoverContents, MarkupKind, SemanticToken, SemanticTokens};
2092
2093 #[test]
2094 fn test_position_map_round_trip() {
2095 let source = "fn python analyze(data: DataTable) -> number {\n import pandas\n return data.mean()\n}\n";
2096 let body_start = source.find("import").unwrap();
2097 let body_end = source.rfind('}').unwrap();
2098 let span = Span::new(body_start, body_end);
2099 let map = PositionMap::new(span, source);
2100
2101 let shape_pos = Position {
2102 line: 1,
2103 character: 4,
2104 };
2105 let virtual_pos = map.shape_to_virtual(shape_pos);
2106 assert!(virtual_pos.is_some());
2107
2108 let back = map.virtual_to_shape(virtual_pos.unwrap());
2109 assert!(back.is_some());
2110 assert_eq!(back.unwrap().line, shape_pos.line);
2111 }
2112
2113 #[test]
2114 fn test_find_foreign_block_at_offset() {
2115 assert!(find_foreign_block_at_offset(&[], 10).is_none());
2117 }
2118
2119 #[test]
2120 fn test_sanitize_filename() {
2121 assert_eq!(
2122 sanitize_filename("file:///home/user/test.shape"),
2123 "file__home_user_test_shape"
2124 );
2125 }
2126
2127 #[test]
2128 fn test_normalize_file_extension() {
2129 assert_eq!(normalize_file_extension(".py"), "py");
2130 assert_eq!(normalize_file_extension("jl"), "jl");
2131 assert_eq!(normalize_file_extension(""), "txt");
2132 }
2133
2134 #[test]
2135 fn test_virtual_doc_mapping_preserves_source_columns_after_dedent() {
2136 let source = r#"fn python percentile(values: Array<number>, pct: number) -> number {
2137 sorted_v = sorted(values)
2138 k = (len(sorted_v) - 1) * (pct / 100.0)
2139 return k
2140}"#;
2141 let program = shape_ast::parser::parse_program(source).expect("program should parse");
2142 let def = match &program.items[0] {
2143 Item::ForeignFunction(def, _) => def,
2144 _ => panic!("expected first item to be foreign function"),
2145 };
2146
2147 let vdoc = generate_virtual_document(
2148 def,
2149 source,
2150 "file:///tmp/test.shape",
2151 std::path::Path::new("/tmp"),
2152 ".py",
2153 );
2154
2155 let first_line_shape = Position {
2156 line: 1,
2157 character: 4,
2158 };
2159 let first_line_virtual = vdoc
2160 .position_map
2161 .shape_to_virtual(first_line_shape)
2162 .expect("first body line should map");
2163 assert_eq!(first_line_virtual.line, 1);
2164 assert_eq!(first_line_virtual.character, 4);
2165 assert_eq!(
2166 vdoc.position_map
2167 .virtual_to_shape(first_line_virtual)
2168 .expect("roundtrip should map"),
2169 first_line_shape
2170 );
2171
2172 let second_line_shape = Position {
2173 line: 2,
2174 character: 4,
2175 };
2176 let second_line_virtual = vdoc
2177 .position_map
2178 .shape_to_virtual(second_line_shape)
2179 .expect("second body line should map");
2180 assert_eq!(second_line_virtual.line, 2);
2181 assert_eq!(second_line_virtual.character, 4);
2182 assert_eq!(
2183 vdoc.position_map
2184 .virtual_to_shape(second_line_virtual)
2185 .expect("roundtrip should map"),
2186 second_line_shape
2187 );
2188 }
2189
2190 #[test]
2191 fn test_virtual_doc_header_uses_async_def_for_async_foreign_functions() {
2192 let source = r#"async fn python fetch_json(url: string) -> Array<number> {
2193 import aiohttp
2194 async with aiohttp.ClientSession() as session:
2195 async with session.get(url) as response:
2196 data = await response.json()
2197 return data["values"]
2198}"#;
2199 let program = shape_ast::parser::parse_program(source).expect("program should parse");
2200 let def = match &program.items[0] {
2201 Item::ForeignFunction(def, _) => def,
2202 _ => panic!("expected first item to be foreign function"),
2203 };
2204
2205 let vdoc = generate_virtual_document(
2206 def,
2207 source,
2208 "file:///tmp/test_async.shape",
2209 std::path::Path::new("/tmp"),
2210 ".py",
2211 );
2212
2213 assert!(
2214 vdoc.content.starts_with("async def fetch_json(url):\n"),
2215 "unexpected virtual document header: {:?}",
2216 vdoc.content.lines().next()
2217 );
2218 }
2219
2220 #[test]
2221 fn test_parse_hover_response_supports_markup_content_and_range() {
2222 let response = serde_json::json!({
2223 "result": {
2224 "contents": {
2225 "kind": "markdown",
2226 "value": "**sorted_v**: `list[float]`"
2227 },
2228 "range": {
2229 "start": { "line": 3, "character": 2 },
2230 "end": { "line": 3, "character": 10 }
2231 }
2232 }
2233 });
2234
2235 let hover = parse_hover_response(response).expect("hover should parse");
2236 match hover.contents {
2237 HoverContents::Markup(markup) => {
2238 assert_eq!(markup.kind, MarkupKind::Markdown);
2239 assert!(markup.value.contains("sorted_v"));
2240 }
2241 other => panic!("expected markup hover, got {other:?}"),
2242 }
2243 assert_eq!(
2244 hover.range,
2245 Some(Range {
2246 start: Position {
2247 line: 3,
2248 character: 2,
2249 },
2250 end: Position {
2251 line: 3,
2252 character: 10,
2253 },
2254 })
2255 );
2256 }
2257
2258 #[test]
2259 fn test_parse_document_diagnostic_report_extracts_items() {
2260 let response = serde_json::json!({
2261 "result": {
2262 "kind": "full",
2263 "items": [
2264 {
2265 "range": {
2266 "start": { "line": 1, "character": 2 },
2267 "end": { "line": 1, "character": 5 }
2268 },
2269 "severity": 1,
2270 "message": "undefined name"
2271 }
2272 ]
2273 }
2274 });
2275
2276 let diagnostics =
2277 parse_document_diagnostic_report(response).expect("diagnostics should parse");
2278 assert_eq!(diagnostics.len(), 1);
2279 assert_eq!(diagnostics[0].message, "undefined name");
2280 assert_eq!(diagnostics[0].range.start.line, 1);
2281 assert_eq!(diagnostics[0].range.start.character, 2);
2282 }
2283
2284 #[test]
2285 fn test_parse_document_diagnostic_report_extracts_nested_full_report_items() {
2286 let response = serde_json::json!({
2287 "result": {
2288 "kind": "workspace",
2289 "fullDocumentDiagnosticReport": {
2290 "kind": "full",
2291 "items": [
2292 {
2293 "range": {
2294 "start": { "line": 2, "character": 0 },
2295 "end": { "line": 2, "character": 4 }
2296 },
2297 "severity": 1,
2298 "message": "bad call"
2299 }
2300 ]
2301 }
2302 }
2303 });
2304
2305 let diagnostics =
2306 parse_document_diagnostic_report(response).expect("nested diagnostics should parse");
2307 assert_eq!(diagnostics.len(), 1);
2308 assert_eq!(diagnostics[0].message, "bad call");
2309 assert_eq!(diagnostics[0].range.start.line, 2);
2310 }
2311
2312 #[test]
2313 fn test_extract_semantic_tokens_legend() {
2314 let init_response = serde_json::json!({
2315 "result": {
2316 "capabilities": {
2317 "semanticTokensProvider": {
2318 "legend": {
2319 "tokenTypes": ["variable", "function"],
2320 "tokenModifiers": ["declaration", "readonly"]
2321 }
2322 }
2323 }
2324 }
2325 });
2326
2327 let (token_types, token_modifiers) = extract_semantic_tokens_legend(&init_response);
2328 assert_eq!(token_types, vec!["variable", "function"]);
2329 assert_eq!(token_modifiers, vec!["declaration", "readonly"]);
2330 }
2331
2332 #[test]
2333 fn test_extract_semantic_tokens_legend_falls_back_when_provider_has_no_legend() {
2334 let init_response = serde_json::json!({
2335 "result": {
2336 "capabilities": {
2337 "semanticTokensProvider": {}
2338 }
2339 }
2340 });
2341
2342 let (token_types, token_modifiers) = extract_semantic_tokens_legend(&init_response);
2343 assert_eq!(
2344 token_types,
2345 CHILD_SEMANTIC_TOKEN_TYPES
2346 .iter()
2347 .map(|s| (*s).to_string())
2348 .collect::<Vec<_>>()
2349 );
2350 assert_eq!(
2351 token_modifiers,
2352 CHILD_SEMANTIC_TOKEN_MODIFIERS
2353 .iter()
2354 .map(|s| (*s).to_string())
2355 .collect::<Vec<_>>()
2356 );
2357 }
2358
2359 #[test]
2360 fn test_extract_semantic_tokens_legend_empty_when_provider_absent() {
2361 let init_response = serde_json::json!({
2362 "result": {
2363 "capabilities": {}
2364 }
2365 });
2366
2367 let (token_types, token_modifiers) = extract_semantic_tokens_legend(&init_response);
2368 assert!(token_types.is_empty());
2369 assert!(token_modifiers.is_empty());
2370 }
2371
2372 #[test]
2373 fn test_child_client_capabilities_request_markdown_hover_and_semantic_tokens() {
2374 let caps = child_client_capabilities();
2375 let hover_formats = caps
2376 .pointer("/textDocument/hover/contentFormat")
2377 .and_then(|v| v.as_array())
2378 .cloned()
2379 .unwrap_or_default();
2380 assert!(
2381 hover_formats.iter().any(|v| v.as_str() == Some("markdown")),
2382 "child capabilities must advertise markdown hover support"
2383 );
2384
2385 assert_eq!(
2386 caps.pointer("/textDocument/semanticTokens/requests/full")
2387 .and_then(|v| v.as_bool()),
2388 Some(true),
2389 "child capabilities must request semanticTokens/full support"
2390 );
2391 assert_eq!(
2392 caps.pointer("/textDocument/semanticTokens/formats/0")
2393 .and_then(|v| v.as_str()),
2394 Some("relative")
2395 );
2396 }
2397
2398 #[test]
2399 fn test_map_foreign_semantic_tokens_to_shape_coordinates() {
2400 let source = "fn python test() {\n x = 1\n}";
2401 let program = shape_ast::parser::parse_program(source).expect("program should parse");
2402 let def = match &program.items[0] {
2403 Item::ForeignFunction(def, _) => def,
2404 _ => panic!("expected first item to be foreign function"),
2405 };
2406 let vdoc = generate_virtual_document(
2407 def,
2408 source,
2409 "file:///tmp/test.shape",
2410 std::path::Path::new("/tmp"),
2411 ".py",
2412 );
2413
2414 let tokens = SemanticTokens {
2415 result_id: None,
2416 data: vec![SemanticToken {
2417 delta_line: 1,
2418 delta_start: 4,
2419 length: 1,
2420 token_type: 0,
2421 token_modifiers_bitset: 1,
2422 }],
2423 };
2424 let mapped = map_foreign_semantic_tokens_to_shape(
2425 &tokens,
2426 &["variable".to_string()],
2427 &["declaration".to_string()],
2428 &vdoc.position_map,
2429 );
2430
2431 assert_eq!(
2432 mapped,
2433 vec![ForeignSemanticToken {
2434 line: 1,
2435 start_char: 4,
2436 length: 1,
2437 token_type: 5,
2438 token_modifiers_bitset: 1,
2439 }]
2440 );
2441 }
2442
2443 #[test]
2444 fn test_map_semantic_token_type_name_supports_common_builtin_aliases() {
2445 assert_eq!(
2446 map_semantic_token_type_name_to_shape_index("builtinFunction"),
2447 Some(4)
2448 );
2449 assert_eq!(
2450 map_semantic_token_type_name_to_shape_index("builtinType"),
2451 Some(1)
2452 );
2453 assert_eq!(
2454 map_semantic_token_type_name_to_shape_index("selfParameter"),
2455 Some(6)
2456 );
2457 assert_eq!(
2458 map_semantic_token_type_name_to_shape_index("builtInType"),
2459 Some(1)
2460 );
2461 assert_eq!(
2462 map_semantic_token_type_name_to_shape_index("builtin"),
2463 Some(5)
2464 );
2465 }
2466
2467 #[test]
2468 fn test_full_document_semantic_tokens_range_covers_entire_document() {
2469 let range = full_document_semantic_tokens_range("a\nbc\n");
2470 assert_eq!(
2471 range,
2472 Range {
2473 start: Position {
2474 line: 0,
2475 character: 0
2476 },
2477 end: Position {
2478 line: 2,
2479 character: 0
2480 }
2481 }
2482 );
2483
2484 let single_line = full_document_semantic_tokens_range("abc");
2485 assert_eq!(
2486 single_line,
2487 Range {
2488 start: Position {
2489 line: 0,
2490 character: 0
2491 },
2492 end: Position {
2493 line: 0,
2494 character: 3
2495 }
2496 }
2497 );
2498 }
2499
2500 #[tokio::test]
2501 async fn test_update_documents_reports_missing_runtime_config_for_foreign_language() {
2502 let source = r#"fn python percentile(values: Array<number>, pct: number) -> number {
2503 return pct
2504}
2505"#;
2506 let program = shape_ast::parser::parse_program(source).expect("program should parse");
2507 let tmp = tempfile::tempdir().expect("tempdir");
2508 let manager = ForeignLspManager::new(tmp.path().to_path_buf());
2509
2510 let diagnostics = manager
2511 .update_documents("file:///tmp/test.shape", source, &program.items, None, None)
2512 .await;
2513
2514 assert_eq!(diagnostics.len(), 1, "expected one startup diagnostic");
2515 assert!(
2516 diagnostics[0]
2517 .message
2518 .contains("No child LSP config for foreign language 'python'"),
2519 "unexpected diagnostic message: {}",
2520 diagnostics[0].message
2521 );
2522 }
2523
2524 #[tokio::test]
2525 async fn test_update_documents_dedupes_missing_runtime_config_diagnostics_per_language() {
2526 let source = r#"fn python p1() -> number {
2527 return 1
2528}
2529fn python p2() -> number {
2530 return 2
2531}
2532"#;
2533 let program = shape_ast::parser::parse_program(source).expect("program should parse");
2534 let tmp = tempfile::tempdir().expect("tempdir");
2535 let manager = ForeignLspManager::new(tmp.path().to_path_buf());
2536
2537 let diagnostics = manager
2538 .update_documents("file:///tmp/test.shape", source, &program.items, None, None)
2539 .await;
2540
2541 assert_eq!(
2542 diagnostics.len(),
2543 1,
2544 "expected deduped missing-runtime diagnostic, got {:?}",
2545 diagnostics
2546 .iter()
2547 .map(|d| d.message.clone())
2548 .collect::<Vec<_>>()
2549 );
2550 }
2551}