Skip to main content

shape_lsp/
foreign_lsp.rs

1//! Foreign language block LSP support.
2//!
3//! Provides position mapping, virtual document generation, type annotation mapping,
4//! and child LSP process management for foreign function blocks. This enables
5//! delegated LSP features (completions, diagnostics, hover) from child language
6//! servers like pyright.
7
8use 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// ---------------------------------------------------------------------------
24// PositionMap
25// ---------------------------------------------------------------------------
26
27/// Maps positions between a Shape source file and a virtual foreign document.
28#[derive(Clone, Debug)]
29pub struct PositionMap {
30    /// For each line in the virtual document, a mapping back to Shape source
31    /// coordinates (or None for synthetic lines like generated headers).
32    line_mapping: Vec<Option<ForeignLineMapping>>,
33    /// Byte offset of the foreign body start in the Shape source.
34    body_start_offset: usize,
35    /// Number of synthetic header lines prepended to the virtual document.
36    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    /// Map a Shape source position to virtual document position, if inside the body.
72    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    /// Map a virtual document position back to Shape source position.
94    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    /// Map a virtual document range back to a Shape source range.
115    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    /// The byte offset where the foreign body starts in the Shape source.
122    pub fn body_start_offset(&self) -> usize {
123        self.body_start_offset
124    }
125
126    /// Number of synthetic header lines in the virtual document.
127    pub fn header_lines(&self) -> u32 {
128        self.header_lines
129    }
130}
131
132// ---------------------------------------------------------------------------
133// VirtualDocument
134// ---------------------------------------------------------------------------
135
136/// A virtual foreign language document generated from a foreign function block.
137pub struct VirtualDocument {
138    /// The virtual document content (e.g., a complete Python file).
139    pub content: String,
140    /// Position mapping between Shape source and virtual document.
141    pub position_map: PositionMap,
142    /// The foreign language identifier (e.g., "python").
143    pub language: String,
144    /// The function name in the Shape source.
145    pub function_name: String,
146    /// URI of the Shape source file this was extracted from.
147    pub source_uri: String,
148    /// Virtual file path used for the child LSP.
149    pub virtual_path: PathBuf,
150}
151
152/// Generate a virtual document from a foreign function definition.
153pub 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    // First line is the synthetic `def` header
186    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
252// ---------------------------------------------------------------------------
253// ForeignLspManager
254// ---------------------------------------------------------------------------
255
256/// State for a single child language server process.
257struct ChildServer {
258    _process: Child,
259    /// Stdin handle for sending JSON-RPC messages.
260    stdin: tokio::process::ChildStdin,
261    /// Pending responses keyed by request ID.
262    pending: HashMap<u64, tokio::sync::oneshot::Sender<serde_json::Value>>,
263    /// Next JSON-RPC request ID.
264    next_id: u64,
265    /// Whether the server has been initialized.
266    initialized: bool,
267    /// Child semantic token type legend (`legend.tokenTypes`).
268    semantic_token_types: Vec<String>,
269    /// Child semantic token modifier legend (`legend.tokenModifiers`).
270    semantic_token_modifiers: Vec<String>,
271    /// Open virtual documents keyed by URI with their current version.
272    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/// Absolute semantic token in Shape document coordinates.
297#[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/// Extension module spec configured directly by the LSP client.
307#[derive(Clone, Debug)]
308pub struct ConfiguredExtensionSpec {
309    pub name: String,
310    pub path: PathBuf,
311    pub config: serde_json::Value,
312}
313
314/// Manages child LSP server processes for foreign language blocks.
315///
316/// Each foreign language gets at most one child server. The manager handles
317/// lifecycle (start, shutdown), document synchronization, and request forwarding.
318pub struct ForeignLspManager {
319    /// Child servers keyed by language identifier.
320    servers: Arc<Mutex<HashMap<String, ChildServer>>>,
321    /// Virtual documents keyed by (source_uri, function_name).
322    documents: Arc<Mutex<HashMap<(String, String), VirtualDocument>>>,
323    /// Last `publishDiagnostics` payloads keyed by virtual document URI.
324    published_diagnostics: Arc<Mutex<HashMap<String, Vec<Diagnostic>>>>,
325    /// Runtime-declared child LSP config keyed by language identifier.
326    runtime_configs: Arc<Mutex<HashMap<String, RuntimeLspConfig>>>,
327    /// Shared extension registry used to discover language runtime LSP configs.
328    extension_registry: shape_runtime::provider_registry::ProviderRegistry,
329    /// Canonical extension identity keys loaded into `extension_registry`.
330    loaded_extension_keys: Arc<Mutex<HashSet<String>>>,
331    /// Extension specs that should always be loaded regardless of source context.
332    configured_extensions: Arc<Mutex<Vec<ConfiguredExtensionSpec>>>,
333    /// Workspace root for virtual document paths and child LSP rootUri.
334    /// Updated from `initialize()` when the real workspace root is known.
335    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    /// Update the workspace root directory.
353    ///
354    /// Called from `initialize()` once the real workspace root is known from
355    /// the client's `rootUri` / `workspaceFolders`. This ensures child LSP
356    /// servers receive the correct `rootUri` so they can discover project
357    /// config files (e.g. `pyrightconfig.json`, virtualenvs).
358    pub fn set_workspace_dir(&self, dir: PathBuf) {
359        *self.workspace_dir.write().unwrap() = dir;
360    }
361
362    /// Set extension specs that should always be loaded for foreign-LSP discovery.
363    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    /// Refresh language-runtime child-LSP configuration from declared extensions.
390    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    /// Start a child language server for the given language, if not already running.
487    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        // Spawn a reader task for stdout
534        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, // EOF
545                    Err(_) => break,
546                    Ok(_) => {}
547                }
548
549                // Parse Content-Length header
550                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                // Read the empty separator line
561                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 this is a response (has "id" but no "method"), route it
597                        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    /// Send an LSP initialize request to a child server.
638    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            // Use the real workspace root so child servers (e.g., pyright) can
650            // discover project config and Python environments.
651            "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        // Send initialized notification
678        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    /// Send a JSON-RPC request to a child server and await the response.
695    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    /// Send a JSON-RPC notification (no response expected) to a child server.
754    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    /// Update virtual documents for all foreign function blocks in a parsed program.
899    ///
900    /// Call this from `analyze_document` whenever the program is successfully parsed.
901    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        // Remove stale documents for this source URI
929        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        // Write virtual documents to disk and notify child servers
954        for ((uri, _fn_name), vdoc) in docs.iter() {
955            if uri != source_uri {
956                continue;
957            }
958            // Ensure parent directory exists
959            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        // Drop lock before async operations
966        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        // Sync virtual documents to child servers with explicit startup errors.
1027        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    /// Handle a completion request that falls inside a foreign function body.
1090    ///
1091    /// Returns `None` if the position is not in a foreign block or delegation fails.
1092    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        // Extract completion items from the response
1114        parse_completion_response(response)
1115    }
1116
1117    /// Handle a hover request that falls inside a foreign function body.
1118    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    /// Handle a signature help request that falls inside a foreign function body.
1147    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    /// Handle go-to-definition for positions inside foreign function bodies.
1171    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    /// Handle find-references for positions inside foreign function bodies.
1197    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    /// Collect semantic tokens for foreign blocks mapped into Shape coordinates.
1229    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            // First semantic-token request can race child-LSP document indexing.
1285            // Retry once after a short delay before giving up.
1286            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        // Some older servers only implement the early draft method name
1343        // `textDocument/semanticTokens` instead of `/full`.
1344        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    /// Retrieve diagnostics from child language servers for foreign blocks in a source file.
1399    ///
1400    /// Maps virtual document diagnostics back to Shape source positions.
1401    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    /// Map a diagnostic from a virtual document back to Shape source coordinates.
1451    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    /// Shut down all child language servers gracefully.
1470    pub async fn shutdown(&self) {
1471        let mut servers = self.servers.lock().await;
1472        for (language, server) in servers.iter_mut() {
1473            // Best-effort shutdown request
1474            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            // Send exit notification
1490            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
1508// ---------------------------------------------------------------------------
1509// Lookup helpers
1510// ---------------------------------------------------------------------------
1511
1512/// Check if a byte offset falls inside any foreign function body in the given items.
1513pub 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
1524/// Check if a position falls inside a foreign function body, given the source text.
1525pub 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
1533// ---------------------------------------------------------------------------
1534// Response parsing helpers
1535// ---------------------------------------------------------------------------
1536
1537fn parse_completion_response(response: serde_json::Value) -> Option<Vec<CompletionItem>> {
1538    let result = response.get("result")?;
1539
1540    // Handle both CompletionList and CompletionItem[] responses
1541    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    // LSP completion item kind values are standardized
1586    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    // Some servers advertise semanticTokensProvider but omit legend fields.
1703    // Use the same baseline vocabulary we advertised in client capabilities.
1704    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,     // bit 0
1997            "definition" => mapped |= 1 << 1, // bit 1
1998            "readonly" => mapped |= 1 << 2,   // bit 2
1999            "static" => mapped |= 1 << 3,     // bit 3
2000            "deprecated" => mapped |= 1 << 4, // bit 4
2001            "defaultLibrary" | "defaultlibrary" | "builtin" => mapped |= 1 << 5, // bit 5
2002            "modification" => mapped |= 1 << 6, // bit 6
2003            _ => {}
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
2043// ---------------------------------------------------------------------------
2044// Utility helpers
2045// ---------------------------------------------------------------------------
2046
2047fn 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
2075/// Sanitize a URI string for use as a filename component.
2076fn sanitize_filename(uri: &str) -> String {
2077    uri.replace("://", "_")
2078        .replace('/', "_")
2079        .replace('\\', "_")
2080        .replace(':', "_")
2081        .replace('.', "_")
2082}
2083
2084// ---------------------------------------------------------------------------
2085// Tests
2086// ---------------------------------------------------------------------------
2087
2088#[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        // Just verify it returns None for empty items
2116        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}