1use 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
54pub struct ShapeLanguageServer {
56 client: Client,
58 documents: DocumentManager,
60 project_root: std::sync::OnceLock<std::path::PathBuf>,
62 last_good_programs: DashMap<Uri, Program>,
65 foreign_lsp: crate::foreign_lsp::ForeignLspManager,
67}
68
69impl ShapeLanguageServer {
70 pub fn new(client: Client) -> Self {
72 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 fn is_shape_toml(uri: &Uri) -> bool {
86 uri.as_str().ends_with("shape.toml")
87 }
88
89 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 async fn analyze_document(&self, uri: &Uri) {
106 let doc = match self.documents.get(uri) {
108 Some(doc) => doc,
109 None => return,
110 };
111
112 let text = doc.text();
114
115 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 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 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 self.last_good_programs.insert(uri.clone(), program.clone());
216
217 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 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 if partial.errors.is_empty() || !has_non_grammar_partial_error {
261 diagnostics.extend(error_to_diagnostic(&error));
262 }
263
264 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 self.last_good_programs
294 .insert(uri.clone(), partial.into_program());
295 }
296
297 diagnostics
298 }
299 };
300
301 let mut all_diagnostics = frontmatter_diagnostics;
303 all_diagnostics.extend(diagnostics);
304
305 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 self.client
316 .log_message(MessageType::INFO, "Shape Language Server initializing")
317 .await;
318
319 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 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 text_document_sync: Some(TextDocumentSyncCapability::Kind(
382 TextDocumentSyncKind::FULL,
383 )),
384
385 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 hover_provider: Some(HoverProviderCapability::Simple(true)),
404
405 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 document_symbol_provider: Some(OneOf::Left(true)),
416
417 workspace_symbol_provider: Some(OneOf::Left(true)),
419
420 definition_provider: Some(OneOf::Left(true)),
422
423 references_provider: Some(OneOf::Left(true)),
425
426 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 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 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 document_formatting_provider: Some(OneOf::Left(true)),
471
472 document_range_formatting_provider: Some(OneOf::Left(true)),
474
475 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 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 code_lens_provider: Some(CodeLensOptions {
491 resolve_provider: Some(true),
492 }),
493
494 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
496
497 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(¶ms.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 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 self.documents.open(uri.clone(), version, text);
557
558 if Self::is_shape_toml(&uri) {
560 self.analyze_toml_document(&uri).await;
561 return;
562 }
563
564 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 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 if Self::is_shape_toml(&uri) {
587 self.analyze_toml_document(&uri).await;
588 return;
589 }
590
591 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 self.client
608 .publish_diagnostics(uri.clone(), vec![], None)
609 .await;
610
611 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 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 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 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 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 }
675 }
676
677 let cached_symbols = self.documents.get_cached_symbols(&uri);
678 let cached_types = self.documents.get_cached_types(&uri);
679
680 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 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 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 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 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 }
754 }
755
756 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 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 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 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 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 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 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 let module_cache = self.documents.get_module_cache();
882
883 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 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 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 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 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 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 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 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 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 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 let edits = format_document(&text, ¶ms.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 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 let edits = format_range(&text, range, ¶ms.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, ¶ms.ch, ¶ms.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 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 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 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 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 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 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 = ¶ms.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, ¶ms.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 = ¶ms.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, ¶ms.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 collect_global_extensions(&mut specs);
1480
1481 dedup_extension_specs(specs)
1482}
1483
1484fn 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 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}