Skip to main content

shape_lsp/
server.rs

1//! Main LSP server implementation
2//!
3//! Implements the Language Server Protocol for Shape.
4
5use crate::annotation_discovery::AnnotationDiscovery;
6use crate::call_hierarchy::{
7    incoming_calls as ch_incoming, outgoing_calls as ch_outgoing,
8    prepare_call_hierarchy as ch_prepare,
9};
10use crate::code_actions::get_code_actions;
11use crate::code_lens::{get_code_lenses, resolve_code_lens};
12use crate::completion::get_completions_with_context;
13use crate::definition::{get_definition, get_references_with_fallback};
14use crate::diagnostics::error_to_diagnostic;
15use crate::document::DocumentManager;
16use crate::document_symbols::{get_document_symbols, get_workspace_symbols};
17use crate::folding::get_folding_ranges;
18use crate::formatting::{format_document, format_on_type, format_range};
19use crate::hover::get_hover;
20use crate::inlay_hints::{InlayHintConfig, get_inlay_hints_with_context};
21use crate::rename::{prepare_rename, rename};
22use crate::semantic_tokens::{get_legend, get_semantic_tokens};
23use crate::signature_help::get_signature_help;
24use crate::util::{
25    mask_leading_prefix_for_parse, offset_to_line_col, parser_source, position_to_offset,
26};
27use dashmap::DashMap;
28use shape_ast::ParseErrorKind;
29use shape_ast::ast::Program;
30use shape_ast::parser::parse_program;
31use std::collections::HashSet;
32use tower_lsp_server::ls_types::{
33    CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
34    CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
35    CallHierarchyServerCapability, CodeActionKind, CodeActionOptions, CodeActionParams,
36    CodeActionProviderCapability, CodeActionResponse, CodeLens, CodeLensOptions, CodeLensParams,
37    CompletionOptions, CompletionParams, CompletionResponse, Diagnostic, DiagnosticSeverity,
38    DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams,
39    DidOpenTextDocumentParams, DocumentFormattingParams, DocumentOnTypeFormattingOptions,
40    DocumentOnTypeFormattingParams, DocumentRangeFormattingParams, DocumentSymbolParams,
41    DocumentSymbolResponse, FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability,
42    GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, HoverProviderCapability,
43    InitializeParams, InitializeResult, InitializedParams, InlayHint, InlayHintOptions,
44    InlayHintParams, InlayHintServerCapabilities, Location, MessageType, OneOf, Position,
45    PrepareRenameResponse, Range, ReferenceParams, RenameOptions, RenameParams, SemanticToken,
46    SemanticTokensFullOptions, SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult,
47    SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, SignatureHelp,
48    SignatureHelpOptions, SignatureHelpParams, TextDocumentPositionParams,
49    TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri, WorkDoneProgressOptions,
50    WorkspaceEdit, WorkspaceSymbolParams, WorkspaceSymbolResponse,
51};
52use tower_lsp_server::{Client, LanguageServer, jsonrpc::Result};
53
54/// The main Shape Language Server
55pub struct ShapeLanguageServer {
56    /// LSP client for sending notifications and requests
57    client: Client,
58    /// Document manager for tracking open files
59    documents: DocumentManager,
60    /// Project root detected from workspace folder via shape.toml
61    project_root: std::sync::OnceLock<std::path::PathBuf>,
62    /// Cache of last successfully parsed programs per URI.
63    /// Used as fallback when current parse fails (e.g., during editing).
64    last_good_programs: DashMap<Uri, Program>,
65    /// Manager for child language servers handling foreign function blocks.
66    foreign_lsp: crate::foreign_lsp::ForeignLspManager,
67}
68
69impl ShapeLanguageServer {
70    /// Create a new language server instance
71    pub fn new(client: Client) -> Self {
72        // Use current directory as default workspace; updated in initialize().
73        let default_workspace =
74            std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
75        Self {
76            client,
77            documents: DocumentManager::new(),
78            project_root: std::sync::OnceLock::new(),
79            last_good_programs: DashMap::new(),
80            foreign_lsp: crate::foreign_lsp::ForeignLspManager::new(default_workspace),
81        }
82    }
83
84    /// Check if a URI points to a shape.toml file.
85    fn is_shape_toml(uri: &Uri) -> bool {
86        uri.as_str().ends_with("shape.toml")
87    }
88
89    /// Analyze a shape.toml document and publish diagnostics.
90    async fn analyze_toml_document(&self, uri: &Uri) {
91        let doc = match self.documents.get(uri) {
92            Some(doc) => doc,
93            None => return,
94        };
95
96        let text = doc.text();
97        let diagnostics = crate::toml_support::diagnostics::validate_toml(&text);
98
99        self.client
100            .publish_diagnostics(uri.clone(), diagnostics, None)
101            .await;
102    }
103
104    /// Analyze a document and publish diagnostics
105    async fn analyze_document(&self, uri: &Uri) {
106        // Get the document
107        let doc = match self.documents.get(uri) {
108            Some(doc) => doc,
109            None => return,
110        };
111
112        // Parse the document
113        let text = doc.text();
114
115        // Validate frontmatter if present (check for project-level sections)
116        let mut frontmatter_diagnostics = Vec::new();
117        let frontmatter_prefix_len;
118        {
119            use shape_runtime::frontmatter::{
120                FrontmatterDiagnosticSeverity, parse_frontmatter, parse_frontmatter_validated,
121            };
122            let (config, fm_diags, rest) = parse_frontmatter_validated(&text);
123            frontmatter_prefix_len = text.len().saturating_sub(rest.len());
124            for diag in fm_diags {
125                let severity = match diag.severity {
126                    FrontmatterDiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
127                    FrontmatterDiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
128                };
129                let range = diag
130                    .location
131                    .map(|loc| Range {
132                        start: Position {
133                            line: loc.line,
134                            character: loc.character,
135                        },
136                        end: Position {
137                            line: loc.line,
138                            character: loc.character + loc.length.max(1),
139                        },
140                    })
141                    .unwrap_or_else(frontmatter_fallback_range);
142                frontmatter_diagnostics.push(Diagnostic {
143                    range,
144                    severity: Some(severity),
145                    message: diag.message,
146                    source: Some("shape".to_string()),
147                    ..Default::default()
148                });
149            }
150
151            // Frontmatter and shape.toml are mutually exclusive.
152            if config.is_some() {
153                if let (Some(project_root), Some(path)) =
154                    (self.project_root.get(), uri.to_file_path())
155                {
156                    if path.as_ref().starts_with(project_root) {
157                        frontmatter_diagnostics.push(Diagnostic {
158                            range: Range {
159                                start: Position {
160                                    line: 0,
161                                    character: 0,
162                                },
163                                end: Position {
164                                    line: 0,
165                                    character: 3,
166                                },
167                            },
168                            severity: Some(DiagnosticSeverity::ERROR),
169                            message: "Frontmatter and shape.toml are mutually exclusive; use one configuration source.".to_string(),
170                            source: Some("shape".to_string()),
171                            ..Default::default()
172                        });
173                    }
174                }
175            }
176
177            // Validate frontmatter extension paths when running as a standalone script.
178            if let (Some(frontmatter), Some(script_path)) =
179                (parse_frontmatter(&text).0, uri.to_file_path())
180            {
181                let path_ranges = frontmatter_extension_path_ranges(&text);
182                if let Some(script_dir) = script_path.as_ref().parent() {
183                    for (index, extension) in frontmatter.extensions.into_iter().enumerate() {
184                        let resolved = if extension.path.is_absolute() {
185                            extension.path.clone()
186                        } else {
187                            script_dir.join(&extension.path)
188                        };
189                        if !resolved.exists() {
190                            let range = path_ranges
191                                .get(index)
192                                .cloned()
193                                .unwrap_or_else(frontmatter_fallback_range);
194                            frontmatter_diagnostics.push(Diagnostic {
195                                range,
196                                severity: Some(DiagnosticSeverity::ERROR),
197                                message: format!(
198                                    "Extension '{}' path does not exist: {}",
199                                    extension.name,
200                                    resolved.display()
201                                ),
202                                source: Some("shape".to_string()),
203                                ..Default::default()
204                            });
205                        }
206                    }
207                }
208            }
209        }
210
211        let parse_source = mask_leading_prefix_for_parse(&text, frontmatter_prefix_len);
212        let diagnostics = match parse_program(parse_source.as_ref()) {
213            Ok(program) => {
214                // Parsing succeeded — cache the good program for fallback
215                self.last_good_programs.insert(uri.clone(), program.clone());
216
217                // Update virtual documents for foreign function blocks
218                let file_path = uri.to_file_path();
219                let foreign_startup_diagnostics = self
220                    .foreign_lsp
221                    .update_documents(
222                        uri.as_str(),
223                        &text,
224                        &program.items,
225                        file_path.as_deref(),
226                        self.project_root.get().map(|p| p.as_path()),
227                    )
228                    .await;
229
230                let module_cache = self.documents.get_module_cache();
231                let mut diagnostics = crate::analysis::analyze_program_semantics(
232                    &program,
233                    &text,
234                    uri.to_file_path().as_deref(),
235                    Some(&module_cache),
236                    self.project_root.get().map(|p| p.as_path()),
237                );
238                diagnostics.extend(crate::doc_diagnostics::validate_program_docs(
239                    &program,
240                    &text,
241                    Some(&module_cache),
242                    uri.to_file_path().as_deref(),
243                    self.project_root.get().map(|p| p.as_path()),
244                ));
245                diagnostics.extend(foreign_startup_diagnostics);
246                diagnostics.extend(self.foreign_lsp.get_diagnostics(uri.as_str()).await);
247                diagnostics
248            }
249            Err(error) => {
250                // Parsing failed — try resilient parse for partial results
251                let partial = shape_ast::parse_program_resilient(parse_source.as_ref());
252                let mut diagnostics = Vec::new();
253                let has_non_grammar_partial_error = partial
254                    .errors
255                    .iter()
256                    .any(|e| !matches!(e.kind, ParseErrorKind::GrammarFailure));
257
258                // Prefer strict parser diagnostics for pure grammar failures.
259                // Add resilient diagnostics when they provide specific recovered spans/messages.
260                if partial.errors.is_empty() || !has_non_grammar_partial_error {
261                    diagnostics.extend(error_to_diagnostic(&error));
262                }
263
264                // Add diagnostics for recovery errors
265                if has_non_grammar_partial_error {
266                    for parse_error in &partial.errors {
267                        if matches!(parse_error.kind, ParseErrorKind::GrammarFailure) {
268                            continue;
269                        }
270                        let (start_line, start_col) = offset_to_line_col(&text, parse_error.span.0);
271                        let (end_line, end_col) = offset_to_line_col(&text, parse_error.span.1);
272                        diagnostics.push(Diagnostic {
273                            range: Range {
274                                start: Position {
275                                    line: start_line,
276                                    character: start_col,
277                                },
278                                end: Position {
279                                    line: end_line,
280                                    character: end_col,
281                                },
282                            },
283                            severity: Some(DiagnosticSeverity::ERROR),
284                            message: parse_error.message.clone(),
285                            source: Some("shape".to_string()),
286                            ..Default::default()
287                        });
288                    }
289                }
290
291                if !partial.items.is_empty() {
292                    // Cache the partial result so hover/completions have something to work with
293                    self.last_good_programs
294                        .insert(uri.clone(), partial.into_program());
295                }
296
297                diagnostics
298            }
299        };
300
301        // Combine frontmatter diagnostics with parse/semantic diagnostics
302        let mut all_diagnostics = frontmatter_diagnostics;
303        all_diagnostics.extend(diagnostics);
304
305        // Publish diagnostics to the client
306        self.client
307            .publish_diagnostics(uri.clone(), all_diagnostics, None)
308            .await;
309    }
310}
311
312impl LanguageServer for ShapeLanguageServer {
313    async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
314        // Log initialization
315        self.client
316            .log_message(MessageType::INFO, "Shape Language Server initializing")
317            .await;
318
319        // Detect project root from workspace folders
320        let mut workspace_folder: Option<std::path::PathBuf> = None;
321        if let Some(folders) = params.workspace_folders.as_ref() {
322            if let Some(folder) = folders.first() {
323                if let Some(folder_path) = folder.uri.to_file_path() {
324                    workspace_folder = Some(folder_path.to_path_buf());
325                    if let Some(project) = shape_runtime::project::find_project_root(&folder_path) {
326                        self.client
327                            .log_message(
328                                MessageType::INFO,
329                                format!(
330                                    "Detected project root: {} ({})",
331                                    project.config.project.name,
332                                    project.root_path.display()
333                                ),
334                            )
335                            .await;
336                        let _ = self.project_root.set(project.root_path);
337                    }
338                }
339            }
340        }
341
342        // Update the foreign LSP manager's workspace dir so child servers
343        // receive the correct rootUri and can discover project config files
344        // (e.g. pyrightconfig.json, virtualenvs).
345        if let Some(dir) = self
346            .project_root
347            .get()
348            .cloned()
349            .or_else(|| workspace_folder.clone())
350        {
351            self.foreign_lsp.set_workspace_dir(dir);
352        }
353
354        let workspace_hint = self
355            .project_root
356            .get()
357            .map(|path| path.as_path())
358            .or(workspace_folder.as_deref());
359        let configured_extensions = configured_extensions_from_lsp_value(
360            params.initialization_options.as_ref(),
361            workspace_hint,
362        );
363        if !configured_extensions.is_empty() {
364            self.client
365                .log_message(
366                    MessageType::INFO,
367                    format!(
368                        "Configured {} always-load extension(s) from LSP initialization options",
369                        configured_extensions.len()
370                    ),
371                )
372                .await;
373        }
374        self.foreign_lsp
375            .set_configured_extensions(configured_extensions)
376            .await;
377
378        Ok(InitializeResult {
379            capabilities: ServerCapabilities {
380                // Use full text document sync (incremental requires proper range handling)
381                text_document_sync: Some(TextDocumentSyncCapability::Kind(
382                    TextDocumentSyncKind::FULL,
383                )),
384
385                // Enable completion support
386                completion_provider: Some(CompletionOptions {
387                    resolve_provider: Some(false),
388                    trigger_characters: Some(vec![
389                        ".".to_string(),
390                        "(".to_string(),
391                        " ".to_string(),
392                        "@".to_string(),
393                        ":".to_string(),
394                    ]),
395                    work_done_progress_options: WorkDoneProgressOptions {
396                        work_done_progress: None,
397                    },
398                    all_commit_characters: None,
399                    completion_item: None,
400                }),
401
402                // Enable hover support
403                hover_provider: Some(HoverProviderCapability::Simple(true)),
404
405                // Enable signature help
406                signature_help_provider: Some(SignatureHelpOptions {
407                    trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
408                    retrigger_characters: None,
409                    work_done_progress_options: WorkDoneProgressOptions {
410                        work_done_progress: None,
411                    },
412                }),
413
414                // Enable document symbols (outline view)
415                document_symbol_provider: Some(OneOf::Left(true)),
416
417                // Enable workspace symbols (find symbol across workspace)
418                workspace_symbol_provider: Some(OneOf::Left(true)),
419
420                // Enable go-to-definition
421                definition_provider: Some(OneOf::Left(true)),
422
423                // Enable find references
424                references_provider: Some(OneOf::Left(true)),
425
426                // Enable semantic tokens for syntax highlighting
427                semantic_tokens_provider: Some(
428                    SemanticTokensServerCapabilities::SemanticTokensOptions(
429                        SemanticTokensOptions {
430                            work_done_progress_options: WorkDoneProgressOptions {
431                                work_done_progress: None,
432                            },
433                            legend: get_legend(),
434                            range: Some(false),
435                            full: Some(SemanticTokensFullOptions::Bool(true)),
436                        },
437                    ),
438                ),
439
440                // Enable inlay hints (type hints, parameter hints)
441                inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
442                    InlayHintOptions {
443                        work_done_progress_options: WorkDoneProgressOptions {
444                            work_done_progress: None,
445                        },
446                        resolve_provider: Some(false),
447                    },
448                ))),
449
450                // Enable code actions (quick fixes, refactoring)
451                code_action_provider: Some(CodeActionProviderCapability::Options(
452                    CodeActionOptions {
453                        code_action_kinds: Some(vec![
454                            CodeActionKind::QUICKFIX,
455                            CodeActionKind::REFACTOR,
456                            CodeActionKind::REFACTOR_EXTRACT,
457                            CodeActionKind::REFACTOR_REWRITE,
458                            CodeActionKind::SOURCE,
459                            CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
460                            CodeActionKind::SOURCE_FIX_ALL,
461                        ]),
462                        work_done_progress_options: WorkDoneProgressOptions {
463                            work_done_progress: None,
464                        },
465                        resolve_provider: Some(false),
466                    },
467                )),
468
469                // Enable document formatting
470                document_formatting_provider: Some(OneOf::Left(true)),
471
472                // Enable range formatting
473                document_range_formatting_provider: Some(OneOf::Left(true)),
474
475                // Enable on-type formatting for indentation while typing.
476                document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
477                    first_trigger_character: "}".to_string(),
478                    more_trigger_character: Some(vec!["\n".to_string()]),
479                }),
480
481                // Enable rename support
482                rename_provider: Some(OneOf::Right(RenameOptions {
483                    prepare_provider: Some(true),
484                    work_done_progress_options: WorkDoneProgressOptions {
485                        work_done_progress: None,
486                    },
487                })),
488
489                // Enable code lens
490                code_lens_provider: Some(CodeLensOptions {
491                    resolve_provider: Some(true),
492                }),
493
494                // Enable folding ranges
495                folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
496
497                // Enable call hierarchy
498                call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
499
500                ..ServerCapabilities::default()
501            },
502            server_info: Some(ServerInfo {
503                name: "Shape Language Server".to_string(),
504                version: Some(env!("CARGO_PKG_VERSION").to_string()),
505            }),
506            ..InitializeResult::default()
507        })
508    }
509
510    async fn initialized(&self, _params: InitializedParams) {
511        self.client
512            .log_message(MessageType::INFO, "Shape Language Server initialized")
513            .await;
514    }
515
516    async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
517        let workspace_hint = self.project_root.get().map(|path| path.as_path());
518        let configured_extensions =
519            configured_extensions_from_lsp_value(Some(&params.settings), workspace_hint);
520        self.foreign_lsp
521            .set_configured_extensions(configured_extensions.clone())
522            .await;
523        self.client
524            .log_message(
525                MessageType::INFO,
526                format!(
527                    "Updated always-load extensions from configuration change ({} configured)",
528                    configured_extensions.len()
529                ),
530            )
531            .await;
532    }
533
534    async fn shutdown(&self) -> Result<()> {
535        self.client
536            .log_message(MessageType::INFO, "Shape Language Server shutting down")
537            .await;
538        // Gracefully shut down all child language servers
539        self.foreign_lsp.shutdown().await;
540        Ok(())
541    }
542
543    async fn did_open(&self, params: DidOpenTextDocumentParams) {
544        let uri = params.text_document.uri;
545        let version = params.text_document.version;
546        let text = params.text_document.text;
547
548        self.client
549            .log_message(
550                MessageType::INFO,
551                format!("Document opened: {}", uri.as_str()),
552            )
553            .await;
554
555        // Store the document
556        self.documents.open(uri.clone(), version, text);
557
558        // Route to TOML analysis for shape.toml files
559        if Self::is_shape_toml(&uri) {
560            self.analyze_toml_document(&uri).await;
561            return;
562        }
563
564        // Analyze and publish diagnostics
565        self.analyze_document(&uri).await;
566    }
567
568    async fn did_change(&self, params: DidChangeTextDocumentParams) {
569        let uri = params.text_document.uri;
570        let version = params.text_document.version;
571
572        // For now, we only handle full document updates
573        // In the future, we can optimize this to handle incremental changes
574        if let Some(change) = params.content_changes.into_iter().next() {
575            let text = change.text;
576            self.documents.update(&uri, version, text);
577
578            self.client
579                .log_message(
580                    MessageType::INFO,
581                    format!("Document changed: {} (version {})", uri.as_str(), version),
582                )
583                .await;
584
585            // Route to TOML analysis for shape.toml files
586            if Self::is_shape_toml(&uri) {
587                self.analyze_toml_document(&uri).await;
588                return;
589            }
590
591            // Analyze and publish diagnostics
592            self.analyze_document(&uri).await;
593        }
594    }
595
596    async fn did_close(&self, params: DidCloseTextDocumentParams) {
597        let uri = params.text_document.uri;
598
599        self.client
600            .log_message(
601                MessageType::INFO,
602                format!("Document closed: {}", uri.as_str()),
603            )
604            .await;
605
606        // Clear diagnostics for the closed document
607        self.client
608            .publish_diagnostics(uri.clone(), vec![], None)
609            .await;
610
611        // Remove cached program for closed document
612        self.last_good_programs.remove(&uri);
613
614        self.documents.close(&uri);
615    }
616
617    async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
618        let uri = params.text_document_position.text_document.uri;
619        let position = params.text_document_position.position;
620
621        self.client
622            .log_message(
623                MessageType::INFO,
624                format!(
625                    "Completion requested at {}:{}:{}",
626                    uri.as_str(),
627                    position.line,
628                    position.character
629                ),
630            )
631            .await;
632
633        // Route to TOML completions for shape.toml files
634        if Self::is_shape_toml(&uri) {
635            let doc = match self.documents.get(&uri) {
636                Some(doc) => doc,
637                None => return Ok(None),
638            };
639            let text = doc.text();
640            let items = crate::toml_support::completions::get_toml_completions(&text, position);
641            return Ok(Some(CompletionResponse::Array(items)));
642        }
643
644        // Get the document
645        let doc = match self.documents.get(&uri) {
646            Some(doc) => doc,
647            None => return Ok(None),
648        };
649
650        let text = doc.text();
651
652        // Route to frontmatter completions for script frontmatter blocks.
653        if is_position_in_frontmatter(&text, position) {
654            let items =
655                crate::toml_support::completions::get_frontmatter_completions(&text, position);
656            return Ok(Some(CompletionResponse::Array(items)));
657        }
658
659        // Check if the cursor is inside a foreign function body
660        if let Some(cached_program) = self.last_good_programs.get(&uri) {
661            if crate::foreign_lsp::is_position_in_foreign_block(
662                &cached_program.items,
663                &text,
664                position,
665            ) {
666                let completions = self
667                    .foreign_lsp
668                    .handle_completion(uri.as_str(), position, &cached_program.items, &text)
669                    .await;
670                if let Some(items) = completions {
671                    return Ok(Some(CompletionResponse::Array(items)));
672                }
673                // If delegation failed, fall through to Shape completions
674            }
675        }
676
677        let cached_symbols = self.documents.get_cached_symbols(&uri);
678        let cached_types = self.documents.get_cached_types(&uri);
679
680        // Get completions (with cached symbols as fallback)
681        let module_cache = self.documents.get_module_cache();
682        let file_path = uri.to_file_path();
683        let (completions, updated_symbols, updated_types) = get_completions_with_context(
684            &text,
685            position,
686            &cached_symbols,
687            &cached_types,
688            Some(&module_cache),
689            file_path.as_deref(),
690            self.project_root.get().map(|p| p.as_path()),
691        );
692
693        // Update cached symbols if parsing succeeded
694        if let Some(symbols) = updated_symbols {
695            self.documents.update_cached_symbols(&uri, symbols);
696        }
697        if let Some(types) = updated_types {
698            self.documents.update_cached_types(&uri, types);
699        }
700
701        Ok(Some(CompletionResponse::Array(completions)))
702    }
703
704    async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
705        let uri = params.text_document_position_params.text_document.uri;
706        let position = params.text_document_position_params.position;
707
708        self.client
709            .log_message(
710                MessageType::INFO,
711                format!(
712                    "Hover requested at {}:{}:{}",
713                    uri.as_str(),
714                    position.line,
715                    position.character
716                ),
717            )
718            .await;
719
720        // Route to TOML hover for shape.toml files
721        if Self::is_shape_toml(&uri) {
722            let doc = match self.documents.get(&uri) {
723                Some(doc) => doc,
724                None => return Ok(None),
725            };
726            let text = doc.text();
727            return Ok(crate::toml_support::hover::get_toml_hover(&text, position));
728        }
729
730        // Get the document
731        let doc = match self.documents.get(&uri) {
732            Some(doc) => doc,
733            None => return Ok(None),
734        };
735
736        let text = doc.text();
737
738        // Check if the cursor is inside a foreign function body
739        if let Some(cached_program) = self.last_good_programs.get(&uri) {
740            if crate::foreign_lsp::is_position_in_foreign_block(
741                &cached_program.items,
742                &text,
743                position,
744            ) {
745                let hover = self
746                    .foreign_lsp
747                    .handle_hover(uri.as_str(), position, &cached_program.items, &text)
748                    .await;
749                if hover.is_some() {
750                    return Ok(hover);
751                }
752                // If delegation failed, fall through to Shape hover
753            }
754        }
755
756        // Get hover information, passing module cache for imported symbol lookup
757        let module_cache = self.documents.get_module_cache();
758        let file_path = uri.to_file_path();
759        let cached = self.last_good_programs.get(&uri);
760        let cached_ref = cached.as_ref().map(|r| r.value());
761        let hover = get_hover(
762            &text,
763            position,
764            Some(&module_cache),
765            file_path.as_deref(),
766            cached_ref,
767        );
768
769        Ok(hover)
770    }
771
772    async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
773        let uri = params.text_document_position_params.text_document.uri;
774        let position = params.text_document_position_params.position;
775
776        // Get the document
777        let doc = match self.documents.get(&uri) {
778            Some(doc) => doc,
779            None => return Ok(None),
780        };
781
782        let text = doc.text();
783
784        if let Some(cached_program) = self.last_good_programs.get(&uri) {
785            if crate::foreign_lsp::is_position_in_foreign_block(
786                &cached_program.items,
787                &text,
788                position,
789            ) {
790                let signature_help = self
791                    .foreign_lsp
792                    .handle_signature_help(uri.as_str(), position, &cached_program.items, &text)
793                    .await;
794                if signature_help.is_some() {
795                    return Ok(signature_help);
796                }
797            }
798        }
799
800        // Get signature help
801        let sig_help = get_signature_help(&text, position);
802
803        Ok(sig_help)
804    }
805
806    async fn document_symbol(
807        &self,
808        params: DocumentSymbolParams,
809    ) -> Result<Option<DocumentSymbolResponse>> {
810        let uri = params.text_document.uri;
811
812        // Get the document
813        let doc = match self.documents.get(&uri) {
814            Some(doc) => doc,
815            None => return Ok(None),
816        };
817
818        let text = doc.text();
819
820        // Get document symbols
821        let symbols = get_document_symbols(&text);
822
823        Ok(symbols)
824    }
825
826    async fn symbol(
827        &self,
828        params: WorkspaceSymbolParams,
829    ) -> Result<Option<WorkspaceSymbolResponse>> {
830        let query = params.query;
831        let mut all_symbols = Vec::new();
832
833        // Get symbols from all open documents
834        for uri in self.documents.all_uris() {
835            if let Some(doc) = self.documents.get(&uri) {
836                let text = doc.text();
837                let symbols = get_workspace_symbols(&text, &uri, &query);
838                all_symbols.extend(symbols);
839            }
840        }
841
842        if all_symbols.is_empty() {
843            Ok(None)
844        } else {
845            Ok(Some(WorkspaceSymbolResponse::Flat(all_symbols)))
846        }
847    }
848
849    async fn goto_definition(
850        &self,
851        params: GotoDefinitionParams,
852    ) -> Result<Option<GotoDefinitionResponse>> {
853        let uri = params.text_document_position_params.text_document.uri;
854        let position = params.text_document_position_params.position;
855
856        // Get the document
857        let doc = match self.documents.get(&uri) {
858            Some(doc) => doc,
859            None => return Ok(None),
860        };
861
862        let text = doc.text();
863
864        if let Some(cached_program) = self.last_good_programs.get(&uri) {
865            if crate::foreign_lsp::is_position_in_foreign_block(
866                &cached_program.items,
867                &text,
868                position,
869            ) {
870                let definition = self
871                    .foreign_lsp
872                    .handle_definition(uri.as_str(), position, &cached_program.items, &text)
873                    .await;
874                if definition.is_some() {
875                    return Ok(definition);
876                }
877            }
878        }
879
880        // Get module cache for cross-file navigation
881        let module_cache = self.documents.get_module_cache();
882
883        // Discover annotations for go-to-definition on annotation names
884        let mut annotation_discovery = AnnotationDiscovery::new();
885        let parse_source = parser_source(&text);
886        if let Ok(program) = parse_program(parse_source.as_ref()) {
887            annotation_discovery.discover_from_program(&program);
888            if let Some(file_path) = uri.to_file_path() {
889                annotation_discovery.discover_from_imports_with_cache(
890                    &program,
891                    &file_path,
892                    &module_cache,
893                    self.project_root.get().map(|p| p.as_path()),
894                );
895            }
896        }
897
898        let cached = self.last_good_programs.get(&uri);
899        let cached_ref = cached.as_ref().map(|r| r.value());
900        let definition = get_definition(
901            &text,
902            position,
903            &uri,
904            Some(&module_cache),
905            Some(&annotation_discovery),
906            cached_ref,
907        );
908
909        Ok(definition)
910    }
911
912    async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
913        let uri = params.text_document_position.text_document.uri;
914        let position = params.text_document_position.position;
915
916        // Get the document
917        let doc = match self.documents.get(&uri) {
918            Some(doc) => doc,
919            None => return Ok(None),
920        };
921
922        let text = doc.text();
923
924        if let Some(cached_program) = self.last_good_programs.get(&uri) {
925            if crate::foreign_lsp::is_position_in_foreign_block(
926                &cached_program.items,
927                &text,
928                position,
929            ) {
930                let references = self
931                    .foreign_lsp
932                    .handle_references(uri.as_str(), position, &cached_program.items, &text)
933                    .await;
934                if references.is_some() {
935                    return Ok(references);
936                }
937            }
938        }
939
940        // Get references with cached program fallback
941        let cached = self.last_good_programs.get(&uri);
942        let cached_ref = cached.as_ref().map(|r| r.value());
943        let references = get_references_with_fallback(&text, position, &uri, cached_ref);
944
945        Ok(references)
946    }
947
948    async fn semantic_tokens_full(
949        &self,
950        params: SemanticTokensParams,
951    ) -> Result<Option<SemanticTokensResult>> {
952        let uri = params.text_document.uri;
953
954        self.client
955            .log_message(
956                MessageType::INFO,
957                format!("Semantic tokens requested for {}", uri.as_str()),
958            )
959            .await;
960
961        // Get the document
962        let doc = match self.documents.get(&uri) {
963            Some(doc) => doc,
964            None => return Ok(None),
965        };
966
967        let text = doc.text();
968
969        // Base Shape semantic tokens
970        let mut tokens = get_semantic_tokens(&text);
971
972        if let Some(ref mut base_tokens) = tokens {
973            let mut absolute = decode_semantic_tokens(&base_tokens.data);
974
975            let frontmatter_tokens =
976                crate::toml_support::semantic_tokens::collect_frontmatter_semantic_tokens(&text);
977            absolute.extend(
978                frontmatter_tokens
979                    .into_iter()
980                    .map(|token| AbsoluteSemanticToken {
981                        line: token.line,
982                        start_char: token.start_char,
983                        length: token.length,
984                        token_type: token.token_type,
985                        modifiers: token.modifiers,
986                    }),
987            );
988
989            if self.last_good_programs.contains_key(&uri) {
990                let foreign_tokens = self.foreign_lsp.collect_semantic_tokens(uri.as_str()).await;
991                absolute.extend(
992                    foreign_tokens
993                        .into_iter()
994                        .map(|token| AbsoluteSemanticToken {
995                            line: token.line,
996                            start_char: token.start_char,
997                            length: token.length,
998                            token_type: token.token_type,
999                            modifiers: token.token_modifiers_bitset,
1000                        }),
1001                );
1002            }
1003
1004            absolute.sort_by_key(|token| {
1005                (
1006                    token.line,
1007                    token.start_char,
1008                    token.length,
1009                    token.token_type,
1010                    token.modifiers,
1011                )
1012            });
1013            absolute.dedup_by_key(|token| {
1014                (
1015                    token.line,
1016                    token.start_char,
1017                    token.length,
1018                    token.token_type,
1019                    token.modifiers,
1020                )
1021            });
1022            base_tokens.data = encode_semantic_tokens(&absolute);
1023        }
1024
1025        Ok(tokens.map(SemanticTokensResult::Tokens))
1026    }
1027
1028    async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
1029        let uri = params.text_document.uri;
1030        let range = params.range;
1031
1032        // Get the document
1033        let doc = match self.documents.get(&uri) {
1034            Some(doc) => doc,
1035            None => return Ok(None),
1036        };
1037
1038        let text = doc.text();
1039
1040        // Get inlay hints with default config, using cached program as fallback
1041        let config = InlayHintConfig::default();
1042        let cached = self.last_good_programs.get(&uri);
1043        let cached_ref = cached.as_ref().map(|r| r.value());
1044        let file_path = uri.to_file_path();
1045        let hints = get_inlay_hints_with_context(
1046            &text,
1047            range,
1048            &config,
1049            cached_ref,
1050            file_path.as_deref(),
1051            self.project_root.get().map(|p| p.as_path()),
1052        );
1053
1054        if hints.is_empty() {
1055            Ok(None)
1056        } else {
1057            Ok(Some(hints))
1058        }
1059    }
1060
1061    async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1062        let uri = params.text_document.uri;
1063        let range = params.range;
1064        let diagnostics = params.context.diagnostics;
1065
1066        // Get the document
1067        let doc = match self.documents.get(&uri) {
1068            Some(doc) => doc,
1069            None => return Ok(None),
1070        };
1071
1072        let text = doc.text();
1073
1074        // Get code actions
1075        let module_cache = self.documents.get_module_cache();
1076        let actions = get_code_actions(
1077            &text,
1078            &uri,
1079            range,
1080            &diagnostics,
1081            Some(&module_cache),
1082            params.context.only.as_deref(),
1083        );
1084
1085        if actions.is_empty() {
1086            Ok(None)
1087        } else {
1088            Ok(Some(actions))
1089        }
1090    }
1091
1092    async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1093        let uri = params.text_document.uri;
1094
1095        // Get the document
1096        let doc = match self.documents.get(&uri) {
1097            Some(doc) => doc,
1098            None => return Ok(None),
1099        };
1100
1101        let text = doc.text();
1102
1103        // Format the document
1104        let edits = format_document(&text, &params.options);
1105
1106        if edits.is_empty() {
1107            Ok(None)
1108        } else {
1109            Ok(Some(edits))
1110        }
1111    }
1112
1113    async fn range_formatting(
1114        &self,
1115        params: DocumentRangeFormattingParams,
1116    ) -> Result<Option<Vec<TextEdit>>> {
1117        let uri = params.text_document.uri;
1118        let range = params.range;
1119
1120        // Get the document
1121        let doc = match self.documents.get(&uri) {
1122            Some(doc) => doc,
1123            None => return Ok(None),
1124        };
1125
1126        let text = doc.text();
1127
1128        // Format the range
1129        let edits = format_range(&text, range, &params.options);
1130
1131        if edits.is_empty() {
1132            Ok(None)
1133        } else {
1134            Ok(Some(edits))
1135        }
1136    }
1137
1138    async fn on_type_formatting(
1139        &self,
1140        params: DocumentOnTypeFormattingParams,
1141    ) -> Result<Option<Vec<TextEdit>>> {
1142        let uri = params.text_document_position.text_document.uri;
1143        let position = params.text_document_position.position;
1144
1145        let doc = match self.documents.get(&uri) {
1146            Some(doc) => doc,
1147            None => return Ok(None),
1148        };
1149
1150        let text = doc.text();
1151        let edits = format_on_type(&text, position, &params.ch, &params.options);
1152
1153        if edits.is_empty() {
1154            Ok(None)
1155        } else {
1156            Ok(Some(edits))
1157        }
1158    }
1159
1160    async fn prepare_rename(
1161        &self,
1162        params: TextDocumentPositionParams,
1163    ) -> Result<Option<PrepareRenameResponse>> {
1164        let uri = params.text_document.uri;
1165        let position = params.position;
1166
1167        // Get the document
1168        let doc = match self.documents.get(&uri) {
1169            Some(doc) => doc,
1170            None => return Ok(None),
1171        };
1172
1173        let text = doc.text();
1174
1175        // Prepare rename
1176        let response = prepare_rename(&text, position);
1177
1178        Ok(response)
1179    }
1180
1181    async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
1182        let uri = params.text_document_position.text_document.uri;
1183        let position = params.text_document_position.position;
1184        let new_name = params.new_name;
1185
1186        // Get the document
1187        let doc = match self.documents.get(&uri) {
1188            Some(doc) => doc,
1189            None => return Ok(None),
1190        };
1191
1192        let text = doc.text();
1193
1194        let cached = self.last_good_programs.get(&uri);
1195        let cached_ref = cached.as_ref().map(|r| r.value());
1196        let edit = rename(&text, &uri, position, &new_name, cached_ref);
1197
1198        Ok(edit)
1199    }
1200
1201    async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
1202        let uri = params.text_document.uri;
1203
1204        // Get the document
1205        let doc = match self.documents.get(&uri) {
1206            Some(doc) => doc,
1207            None => return Ok(None),
1208        };
1209
1210        let text = doc.text();
1211
1212        // Get code lenses
1213        let lenses = get_code_lenses(&text, &uri);
1214
1215        if lenses.is_empty() {
1216            Ok(None)
1217        } else {
1218            Ok(Some(lenses))
1219        }
1220    }
1221
1222    async fn code_lens_resolve(&self, lens: CodeLens) -> Result<CodeLens> {
1223        Ok(resolve_code_lens(lens))
1224    }
1225
1226    async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
1227        let uri = params.text_document.uri;
1228
1229        let doc = match self.documents.get(&uri) {
1230            Some(doc) => doc,
1231            None => return Ok(None),
1232        };
1233
1234        let text = doc.text();
1235        let parse_source = parser_source(&text);
1236        let program = match parse_program(parse_source.as_ref()) {
1237            Ok(p) => p,
1238            Err(_) => {
1239                // Fall back to cached program
1240                match self.last_good_programs.get(&uri) {
1241                    Some(cached) => cached.value().clone(),
1242                    None => return Ok(None),
1243                }
1244            }
1245        };
1246
1247        let ranges = get_folding_ranges(&text, &program);
1248        if ranges.is_empty() {
1249            Ok(None)
1250        } else {
1251            Ok(Some(ranges))
1252        }
1253    }
1254
1255    async fn prepare_call_hierarchy(
1256        &self,
1257        params: CallHierarchyPrepareParams,
1258    ) -> Result<Option<Vec<CallHierarchyItem>>> {
1259        let uri = params.text_document_position_params.text_document.uri;
1260        let position = params.text_document_position_params.position;
1261
1262        let doc = match self.documents.get(&uri) {
1263            Some(doc) => doc,
1264            None => return Ok(None),
1265        };
1266
1267        let text = doc.text();
1268        Ok(ch_prepare(&text, position, &uri))
1269    }
1270
1271    async fn incoming_calls(
1272        &self,
1273        params: CallHierarchyIncomingCallsParams,
1274    ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1275        let uri = &params.item.uri;
1276
1277        let doc = match self.documents.get(uri) {
1278            Some(doc) => doc,
1279            None => return Ok(None),
1280        };
1281
1282        let text = doc.text();
1283        let results = ch_incoming(&text, &params.item, uri);
1284        if results.is_empty() {
1285            Ok(None)
1286        } else {
1287            Ok(Some(results))
1288        }
1289    }
1290
1291    async fn outgoing_calls(
1292        &self,
1293        params: CallHierarchyOutgoingCallsParams,
1294    ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
1295        let uri = &params.item.uri;
1296
1297        let doc = match self.documents.get(uri) {
1298            Some(doc) => doc,
1299            None => return Ok(None),
1300        };
1301
1302        let text = doc.text();
1303        let results = ch_outgoing(&text, &params.item, uri);
1304        if results.is_empty() {
1305            Ok(None)
1306        } else {
1307            Ok(Some(results))
1308        }
1309    }
1310}
1311
1312fn frontmatter_fallback_range() -> Range {
1313    Range {
1314        start: Position {
1315            line: 0,
1316            character: 0,
1317        },
1318        end: Position {
1319            line: 0,
1320            character: 3,
1321        },
1322    }
1323}
1324
1325#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1326struct AbsoluteSemanticToken {
1327    line: u32,
1328    start_char: u32,
1329    length: u32,
1330    token_type: u32,
1331    modifiers: u32,
1332}
1333
1334fn decode_semantic_tokens(tokens: &[SemanticToken]) -> Vec<AbsoluteSemanticToken> {
1335    let mut decoded = Vec::with_capacity(tokens.len());
1336    let mut line = 0u32;
1337    let mut col = 0u32;
1338
1339    for token in tokens {
1340        line += token.delta_line;
1341        if token.delta_line == 0 {
1342            col += token.delta_start;
1343        } else {
1344            col = token.delta_start;
1345        }
1346        decoded.push(AbsoluteSemanticToken {
1347            line,
1348            start_char: col,
1349            length: token.length,
1350            token_type: token.token_type,
1351            modifiers: token.token_modifiers_bitset,
1352        });
1353    }
1354
1355    decoded
1356}
1357
1358fn encode_semantic_tokens(tokens: &[AbsoluteSemanticToken]) -> Vec<SemanticToken> {
1359    let mut encoded = Vec::with_capacity(tokens.len());
1360    let mut prev_line = 0u32;
1361    let mut prev_start = 0u32;
1362
1363    for token in tokens {
1364        let delta_line = token.line.saturating_sub(prev_line);
1365        let delta_start = if delta_line == 0 {
1366            token.start_char.saturating_sub(prev_start)
1367        } else {
1368            token.start_char
1369        };
1370        encoded.push(SemanticToken {
1371            delta_line,
1372            delta_start,
1373            length: token.length,
1374            token_type: token.token_type,
1375            token_modifiers_bitset: token.modifiers,
1376        });
1377        prev_line = token.line;
1378        prev_start = token.start_char;
1379    }
1380
1381    encoded
1382}
1383
1384fn frontmatter_extension_path_ranges(source: &str) -> Vec<Range> {
1385    let lines: Vec<&str> = source.split('\n').collect();
1386    if lines.is_empty() {
1387        return Vec::new();
1388    }
1389
1390    let delimiter_lines: Vec<usize> = lines
1391        .iter()
1392        .enumerate()
1393        .filter_map(|(idx, line)| {
1394            let trimmed = line.trim_end_matches('\r').trim();
1395            if trimmed == "---" { Some(idx) } else { None }
1396        })
1397        .take(2)
1398        .collect();
1399
1400    if delimiter_lines.len() < 2 {
1401        return Vec::new();
1402    }
1403
1404    let start = delimiter_lines[0] + 1;
1405    let end = delimiter_lines[1];
1406    let mut in_extensions = false;
1407    let mut ranges = Vec::new();
1408
1409    for (line_idx, raw_line) in lines.iter().enumerate().take(end).skip(start) {
1410        let trimmed = raw_line.trim();
1411        if trimmed.starts_with("[[extensions]]") {
1412            in_extensions = true;
1413            continue;
1414        }
1415
1416        if trimmed.starts_with("[[") || (trimmed.starts_with('[') && trimmed.ends_with(']')) {
1417            in_extensions = false;
1418            continue;
1419        }
1420
1421        if !in_extensions {
1422            continue;
1423        }
1424
1425        let Some(eq_pos) = raw_line.find('=') else {
1426            continue;
1427        };
1428        let key = raw_line[..eq_pos].trim();
1429        if key != "path" {
1430            continue;
1431        }
1432
1433        let key_start = raw_line.find("path").unwrap_or_else(|| {
1434            raw_line[..eq_pos]
1435                .find(|c: char| !c.is_whitespace())
1436                .unwrap_or(0)
1437        });
1438        let line_len = raw_line.trim_end_matches('\r').len();
1439        let end_char = line_len.max(key_start + 4);
1440
1441        ranges.push(Range {
1442            start: Position {
1443                line: line_idx as u32,
1444                character: key_start as u32,
1445            },
1446            end: Position {
1447                line: line_idx as u32,
1448                character: end_char as u32,
1449            },
1450        });
1451    }
1452
1453    ranges
1454}
1455
1456fn is_position_in_frontmatter(source: &str, position: Position) -> bool {
1457    if source.starts_with("#!") && position.line == 0 {
1458        return false;
1459    }
1460
1461    let (_, _, rest) = shape_runtime::frontmatter::parse_frontmatter_validated(source);
1462    let prefix_len = source.len().saturating_sub(rest.len());
1463    if prefix_len == 0 {
1464        return false;
1465    }
1466
1467    position_to_offset(source, position)
1468        .map(|offset| offset < prefix_len)
1469        .unwrap_or(false)
1470}
1471
1472fn configured_extensions_from_lsp_value(
1473    options: Option<&serde_json::Value>,
1474    workspace_root: Option<&std::path::Path>,
1475) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
1476    let mut specs = collect_configured_extensions_from_options(options, workspace_root);
1477
1478    // Auto-discover globally installed extensions from ~/.shape/extensions/
1479    collect_global_extensions(&mut specs);
1480
1481    dedup_extension_specs(specs)
1482}
1483
1484/// Parse configured extensions from LSP options JSON only (no global discovery).
1485fn collect_configured_extensions_from_options(
1486    options: Option<&serde_json::Value>,
1487    workspace_root: Option<&std::path::Path>,
1488) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
1489    let mut specs = Vec::new();
1490
1491    if let Some(options) = options {
1492        collect_configured_extensions_from_array(
1493            options.get("alwaysLoadExtensions"),
1494            workspace_root,
1495            &mut specs,
1496        );
1497        collect_configured_extensions_from_array(
1498            options.get("always_load_extensions"),
1499            workspace_root,
1500            &mut specs,
1501        );
1502
1503        if let Some(shape) = options.get("shape") {
1504            collect_configured_extensions_from_array(
1505                shape.get("alwaysLoadExtensions"),
1506                workspace_root,
1507                &mut specs,
1508            );
1509            collect_configured_extensions_from_array(
1510                shape.get("always_load_extensions"),
1511                workspace_root,
1512                &mut specs,
1513            );
1514        }
1515    }
1516
1517    specs
1518}
1519
1520fn dedup_extension_specs(
1521    specs: Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
1522) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
1523    let mut seen = HashSet::new();
1524    specs
1525        .into_iter()
1526        .filter(|spec| {
1527            let key = format!(
1528                "{}|{}|{}",
1529                spec.name,
1530                spec.path.display(),
1531                serde_json::to_string(&spec.config).unwrap_or_default()
1532            );
1533            seen.insert(key)
1534        })
1535        .collect()
1536}
1537
1538fn collect_global_extensions(out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>) {
1539    let Some(home) = dirs::home_dir() else {
1540        return;
1541    };
1542    let ext_dir = home.join(".shape").join("extensions");
1543    if !ext_dir.is_dir() {
1544        return;
1545    }
1546    let Ok(entries) = std::fs::read_dir(&ext_dir) else {
1547        return;
1548    };
1549    for entry in entries.flatten() {
1550        let path = entry.path();
1551        let is_lib = path
1552            .extension()
1553            .and_then(|e| e.to_str())
1554            .map(|ext| ext == "so" || ext == "dylib" || ext == "dll")
1555            .unwrap_or(false);
1556        if !is_lib {
1557            continue;
1558        }
1559        let name = path
1560            .file_stem()
1561            .and_then(|s| s.to_str())
1562            .map(|s| {
1563                s.strip_prefix("libshape_ext_")
1564                    .or_else(|| s.strip_prefix("shape_ext_"))
1565                    .unwrap_or(s)
1566                    .to_string()
1567            })
1568            .unwrap_or_else(|| "extension".to_string());
1569        out.push(crate::foreign_lsp::ConfiguredExtensionSpec {
1570            name,
1571            path,
1572            config: serde_json::json!({}),
1573        });
1574    }
1575}
1576
1577fn collect_configured_extensions_from_array(
1578    value: Option<&serde_json::Value>,
1579    workspace_root: Option<&std::path::Path>,
1580    out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
1581) {
1582    let Some(items) = value.and_then(|v| v.as_array()) else {
1583        return;
1584    };
1585    for item in items {
1586        if let Some(spec) = parse_configured_extension_item(item, workspace_root) {
1587            out.push(spec);
1588        }
1589    }
1590}
1591
1592fn parse_configured_extension_item(
1593    item: &serde_json::Value,
1594    workspace_root: Option<&std::path::Path>,
1595) -> Option<crate::foreign_lsp::ConfiguredExtensionSpec> {
1596    let (name, path, config) = if let Some(path) = item.as_str() {
1597        let path_buf = resolve_configured_extension_path(path, workspace_root);
1598        (
1599            configured_extension_name_from_path(&path_buf),
1600            path_buf,
1601            serde_json::json!({}),
1602        )
1603    } else if let Some(obj) = item.as_object() {
1604        let path_str = obj.get("path")?.as_str()?;
1605        let path_buf = resolve_configured_extension_path(path_str, workspace_root);
1606        let name = obj
1607            .get("name")
1608            .and_then(|value| value.as_str())
1609            .map(String::from)
1610            .unwrap_or_else(|| configured_extension_name_from_path(&path_buf));
1611        let config = obj
1612            .get("config")
1613            .cloned()
1614            .unwrap_or_else(|| serde_json::json!({}));
1615        (name, path_buf, config)
1616    } else {
1617        return None;
1618    };
1619
1620    Some(crate::foreign_lsp::ConfiguredExtensionSpec { name, path, config })
1621}
1622
1623fn resolve_configured_extension_path(
1624    path: &str,
1625    workspace_root: Option<&std::path::Path>,
1626) -> std::path::PathBuf {
1627    let path = std::path::PathBuf::from(path);
1628    if path.is_absolute() {
1629        return path;
1630    }
1631    workspace_root.map(|root| root.join(&path)).unwrap_or(path)
1632}
1633
1634fn configured_extension_name_from_path(path: &std::path::Path) -> String {
1635    path.file_stem()
1636        .and_then(|stem| stem.to_str())
1637        .map(String::from)
1638        .unwrap_or_else(|| "configured-extension".to_string())
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644    use crate::util::parser_source;
1645    use tower_lsp_server::LspService;
1646
1647    #[tokio::test]
1648    async fn test_server_creation() {
1649        let (service, _socket) = LspService::new(|client| ShapeLanguageServer::new(client));
1650
1651        // Just verify we can create the service
1652        drop(service);
1653    }
1654
1655    #[test]
1656    fn test_frontmatter_extension_path_ranges_points_to_path_line() {
1657        let source = r#"---
1658[[extensions]]
1659name = "duckdb"
1660path = "./extensions/libshape_ext_duckdb.so"
1661---
1662let x = 1
1663"#;
1664
1665        let ranges = frontmatter_extension_path_ranges(source);
1666        assert_eq!(ranges.len(), 1);
1667        assert_eq!(ranges[0].start.line, 3);
1668        assert_eq!(ranges[0].start.character, 0);
1669    }
1670
1671    #[test]
1672    fn test_frontmatter_extension_path_ranges_handles_shebang() {
1673        let source = r#"#!/usr/bin/env shape
1674---
1675[[extensions]]
1676name = "duckdb"
1677path = "./extensions/libshape_ext_duckdb.so"
1678---
1679let x = 1
1680"#;
1681
1682        let ranges = frontmatter_extension_path_ranges(source);
1683        assert_eq!(ranges.len(), 1);
1684        assert_eq!(ranges[0].start.line, 4);
1685    }
1686
1687    #[test]
1688    fn test_validate_imports_accepts_namespace_import_from_frontmatter_extension() {
1689        let source = r#"---
1690# shape.toml
1691[[extensions]]
1692name = "duckdb"
1693path = "./extensions/libshape_ext_duckdb.so"
1694---
1695use duckdb
1696let conn = duckdb.connect("duckdb://analytics.db")
1697"#;
1698
1699        let tmp = tempfile::tempdir().unwrap();
1700        let file_path = tmp.path().join("script.shape");
1701        std::fs::write(&file_path, source).unwrap();
1702
1703        let parse_src = parser_source(source);
1704        let program = parse_program(parse_src.as_ref()).expect("program should parse");
1705        let module_cache = crate::module_cache::ModuleCache::new();
1706        let mut compiler = shape_vm::BytecodeCompiler::new();
1707
1708        let diagnostics = crate::analysis::validate_imports_and_register_items(
1709            &program,
1710            source,
1711            &file_path,
1712            &module_cache,
1713            None,
1714            &mut compiler,
1715        );
1716
1717        assert!(
1718            diagnostics.iter().all(|diag| {
1719                !diag.message.contains("Cannot resolve module ''")
1720                    && !diag.message.contains("Cannot resolve module 'duckdb'")
1721            }),
1722            "namespace import from frontmatter extension should not emit resolution errors: {:?}",
1723            diagnostics
1724        );
1725    }
1726
1727    #[test]
1728    fn test_validate_imports_reports_unknown_namespace_module_name() {
1729        let source = "use missingmod\nlet x = 1\n";
1730        let tmp = tempfile::tempdir().unwrap();
1731        let file_path = tmp.path().join("script.shape");
1732        std::fs::write(&file_path, source).unwrap();
1733
1734        let parse_src = parser_source(source);
1735        let program = parse_program(parse_src.as_ref()).expect("program should parse");
1736        let module_cache = crate::module_cache::ModuleCache::new();
1737        let mut compiler = shape_vm::BytecodeCompiler::new();
1738
1739        let diagnostics = crate::analysis::validate_imports_and_register_items(
1740            &program,
1741            source,
1742            &file_path,
1743            &module_cache,
1744            None,
1745            &mut compiler,
1746        );
1747
1748        assert!(
1749            diagnostics
1750                .iter()
1751                .any(|diag| diag.message.contains("Cannot resolve module 'missingmod'")),
1752            "expected unknown namespace module diagnostic, got {:?}",
1753            diagnostics
1754        );
1755    }
1756
1757    #[test]
1758    fn test_is_position_in_frontmatter() {
1759        let source = r#"---
1760name = "script"
1761[[extensions]]
1762name = "duckdb"
1763path = "./extensions/libshape_ext_duckdb.so"
1764---
1765let x = 1
1766"#;
1767
1768        assert!(is_position_in_frontmatter(
1769            source,
1770            Position {
1771                line: 1,
1772                character: 0
1773            }
1774        ));
1775        assert!(is_position_in_frontmatter(
1776            source,
1777            Position {
1778                line: 3,
1779                character: 2
1780            }
1781        ));
1782        assert!(!is_position_in_frontmatter(
1783            source,
1784            Position {
1785                line: 6,
1786                character: 0
1787            }
1788        ));
1789    }
1790
1791    #[test]
1792    fn test_is_position_in_frontmatter_ignores_shebang_line() {
1793        let source = r#"#!/usr/bin/env shape
1794---
1795name = "script"
1796---
1797print("hello")
1798"#;
1799
1800        assert!(!is_position_in_frontmatter(
1801            source,
1802            Position {
1803                line: 0,
1804                character: 5
1805            }
1806        ));
1807        assert!(is_position_in_frontmatter(
1808            source,
1809            Position {
1810                line: 2,
1811                character: 1
1812            }
1813        ));
1814        assert!(!is_position_in_frontmatter(
1815            source,
1816            Position {
1817                line: 4,
1818                character: 0
1819            }
1820        ));
1821    }
1822
1823    #[test]
1824    fn test_configured_extensions_from_lsp_value_parses_top_level_array() {
1825        let value = serde_json::json!({
1826            "alwaysLoadExtensions": [
1827                "./extensions/libshape_ext_python.so",
1828                {
1829                    "name": "duckdb",
1830                    "path": "/tmp/libshape_ext_duckdb.so",
1831                    "config": { "mode": "readonly" }
1832                }
1833            ]
1834        });
1835        let workspace_root = std::path::Path::new("/workspace");
1836
1837        let specs = collect_configured_extensions_from_options(Some(&value), Some(workspace_root));
1838        assert_eq!(specs.len(), 2);
1839        assert_eq!(
1840            specs[0].path,
1841            std::path::PathBuf::from("/workspace").join("./extensions/libshape_ext_python.so")
1842        );
1843        assert_eq!(specs[0].config, serde_json::json!({}));
1844        assert_eq!(specs[1].name, "duckdb");
1845        assert_eq!(
1846            specs[1].path,
1847            std::path::PathBuf::from("/tmp/libshape_ext_duckdb.so")
1848        );
1849        assert_eq!(specs[1].config, serde_json::json!({ "mode": "readonly" }));
1850    }
1851
1852    #[test]
1853    fn test_configured_extensions_from_lsp_value_parses_nested_shape_key_and_dedupes() {
1854        let value = serde_json::json!({
1855            "shape": {
1856                "always_load_extensions": [
1857                    "/tmp/libshape_ext_python.so",
1858                    "/tmp/libshape_ext_python.so"
1859                ]
1860            }
1861        });
1862
1863        let specs = dedup_extension_specs(collect_configured_extensions_from_options(
1864            Some(&value),
1865            None,
1866        ));
1867        assert_eq!(specs.len(), 1);
1868        assert_eq!(
1869            specs[0].path,
1870            std::path::PathBuf::from("/tmp/libshape_ext_python.so")
1871        );
1872        assert_eq!(specs[0].name, "libshape_ext_python");
1873    }
1874}