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::{
14 get_declaration, get_definition, get_document_highlights, get_implementations,
15 get_references_cross_file, get_type_definition,
16};
17use crate::diagnostics::error_to_diagnostic;
18use crate::document::DocumentManager;
19use crate::document_symbols::{get_document_symbols, get_workspace_symbols};
20use crate::folding::get_folding_ranges;
21use crate::formatting::{format_document, format_on_type, format_range};
22use crate::hover::get_hover;
23use crate::inlay_hints::{InlayHintConfig, get_inlay_hints_with_context};
24use crate::rename::{prepare_rename, rename_cross_file};
25use crate::semantic_tokens::{get_legend, get_semantic_tokens};
26use crate::signature_help::get_signature_help;
27use crate::util::{
28 mask_leading_prefix_for_parse, offset_to_line_col, parser_source, position_to_offset,
29};
30use dashmap::DashMap;
31use shape_ast::ParseErrorKind;
32use shape_ast::ast::Program;
33use shape_ast::parser::parse_program;
34use std::collections::HashSet;
35use tower_lsp_server::ls_types::request::{
36 GotoDeclarationParams, GotoDeclarationResponse, GotoImplementationParams,
37 GotoImplementationResponse, GotoTypeDefinitionParams, GotoTypeDefinitionResponse,
38};
39use tower_lsp_server::ls_types::{
40 CallHierarchyIncomingCall, CallHierarchyIncomingCallsParams, CallHierarchyItem,
41 CallHierarchyOutgoingCall, CallHierarchyOutgoingCallsParams, CallHierarchyPrepareParams,
42 CallHierarchyServerCapability, CodeActionKind, CodeActionOptions, CodeActionParams,
43 CodeActionProviderCapability, CodeActionResponse, CodeLens, CodeLensOptions, CodeLensParams,
44 CompletionItem, CompletionOptions, CompletionParams, CompletionResponse, DeclarationCapability,
45 Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities, DiagnosticSeverity,
46 DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams,
47 DidCloseTextDocumentParams, DidOpenTextDocumentParams, DocumentDiagnosticParams,
48 DocumentDiagnosticReport, DocumentDiagnosticReportResult, DocumentFormattingParams,
49 DocumentHighlight, DocumentHighlightParams, DocumentLink, DocumentLinkOptions,
50 DocumentLinkParams, DocumentOnTypeFormattingOptions, DocumentOnTypeFormattingParams,
51 DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse,
52 ExecuteCommandOptions, ExecuteCommandParams, FileChangeType, FileOperationFilter,
53 FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions,
54 FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability, FullDocumentDiagnosticReport,
55 GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverParams, HoverProviderCapability,
56 ImplementationProviderCapability, InitializeParams, InitializeResult, InitializedParams,
57 InlayHint, InlayHintOptions, InlayHintParams, InlayHintServerCapabilities, LSPAny, Location,
58 MessageType, OneOf, Position, PrepareRenameResponse, Range, ReferenceParams,
59 RelatedFullDocumentDiagnosticReport, RenameFilesParams, RenameOptions, RenameParams,
60 SemanticToken, SemanticTokens, SemanticTokensDelta, SemanticTokensDeltaParams,
61 SemanticTokensEdit, SemanticTokensFullDeltaResult, SemanticTokensFullOptions,
62 SemanticTokensOptions, SemanticTokensParams, SemanticTokensRangeParams,
63 SemanticTokensRangeResult, SemanticTokensResult, SemanticTokensServerCapabilities,
64 ServerCapabilities, ServerInfo, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
65 TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit,
66 TypeDefinitionProviderCapability, Uri, WorkDoneProgressOptions, WorkspaceDiagnosticParams,
67 WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
68 WorkspaceFileOperationsServerCapabilities, WorkspaceEdit, WorkspaceFullDocumentDiagnosticReport,
69 WorkspaceServerCapabilities, WorkspaceSymbolParams, WorkspaceSymbolResponse,
70};
71use tower_lsp_server::{Client, LanguageServer, jsonrpc::Result};
72
73pub struct ShapeLanguageServer {
75 client: Client,
77 documents: DocumentManager,
79 project_root: std::sync::OnceLock<std::path::PathBuf>,
81 last_good_programs: DashMap<Uri, Program>,
84 foreign_lsp: crate::foreign_lsp::ForeignLspManager,
86 semantic_tokens_cache: DashMap<Uri, CachedSemanticTokens>,
90 semantic_tokens_result_counter: std::sync::atomic::AtomicU64,
92 inlay_hint_config: std::sync::RwLock<InlayHintConfig>,
95}
96
97#[derive(Clone)]
100struct CachedSemanticTokens {
101 result_id: String,
104 data: Vec<SemanticToken>,
107}
108
109impl ShapeLanguageServer {
110 pub fn new(client: Client) -> Self {
112 let default_workspace =
114 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
115 Self {
116 client,
117 documents: DocumentManager::new(),
118 project_root: std::sync::OnceLock::new(),
119 last_good_programs: DashMap::new(),
120 foreign_lsp: crate::foreign_lsp::ForeignLspManager::new(default_workspace),
121 semantic_tokens_cache: DashMap::new(),
122 semantic_tokens_result_counter: std::sync::atomic::AtomicU64::new(0),
123 inlay_hint_config: std::sync::RwLock::new(InlayHintConfig::default()),
124 }
125 }
126
127 fn next_semantic_tokens_result_id(&self) -> String {
131 let n = self
132 .semantic_tokens_result_counter
133 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
134 format!("st-{}", n)
135 }
136
137 async fn collect_full_semantic_tokens(&self, uri: &Uri) -> Option<SemanticTokens> {
146 let doc = self.documents.get(uri)?;
147 let text = doc.text();
148
149 let mut tokens = get_semantic_tokens(&text)?;
150 let mut absolute = decode_semantic_tokens(&tokens.data);
151
152 let frontmatter_tokens =
153 crate::toml_support::semantic_tokens::collect_frontmatter_semantic_tokens(&text);
154 absolute.extend(
155 frontmatter_tokens
156 .into_iter()
157 .map(|token| AbsoluteSemanticToken {
158 line: token.line,
159 start_char: token.start_char,
160 length: token.length,
161 token_type: token.token_type,
162 modifiers: token.modifiers,
163 }),
164 );
165
166 if self.last_good_programs.contains_key(uri) {
167 let foreign_tokens = self.foreign_lsp.collect_semantic_tokens(uri.as_str()).await;
168 absolute.extend(
169 foreign_tokens
170 .into_iter()
171 .map(|token| AbsoluteSemanticToken {
172 line: token.line,
173 start_char: token.start_char,
174 length: token.length,
175 token_type: token.token_type,
176 modifiers: token.token_modifiers_bitset,
177 }),
178 );
179 }
180
181 absolute.sort_by_key(|token| {
182 (
183 token.line,
184 token.start_char,
185 token.length,
186 token.token_type,
187 token.modifiers,
188 )
189 });
190 absolute.dedup_by_key(|token| {
191 (
192 token.line,
193 token.start_char,
194 token.length,
195 token.token_type,
196 token.modifiers,
197 )
198 });
199 tokens.data = encode_semantic_tokens(&absolute);
200 Some(tokens)
201 }
202
203 fn is_shape_toml(uri: &Uri) -> bool {
205 uri.as_str().ends_with("shape.toml")
206 }
207
208 async fn analyze_toml_document(&self, uri: &Uri) {
210 let doc = match self.documents.get(uri) {
211 Some(doc) => doc,
212 None => return,
213 };
214
215 let text = doc.text();
216 let diagnostics = crate::toml_support::diagnostics::validate_toml(&text);
217
218 self.client
219 .publish_diagnostics(uri.clone(), diagnostics, None)
220 .await;
221 }
222
223 async fn compute_pull_diagnostics(&self, uri: &Uri) -> Vec<Diagnostic> {
234 let Some(doc) = self.documents.get(uri) else {
235 return Vec::new();
236 };
237 let text = doc.text();
238
239 let parse_source = mask_leading_prefix_for_parse(&text, 0);
241 match parse_program(parse_source.as_ref()) {
242 Ok(program) => {
243 let module_cache = self.documents.get_module_cache();
244 let mut diagnostics = crate::analysis::analyze_program_semantics(
245 &program,
246 &text,
247 uri.to_file_path().as_deref(),
248 Some(&module_cache),
249 self.project_root.get().map(|p| p.as_path()),
250 );
251 diagnostics.extend(crate::doc_diagnostics::validate_program_docs(
252 &program,
253 &text,
254 Some(&module_cache),
255 uri.to_file_path().as_deref(),
256 self.project_root.get().map(|p| p.as_path()),
257 ));
258 diagnostics.extend(self.foreign_lsp.get_diagnostics(uri.as_str()).await);
259 diagnostics
260 }
261 Err(error) => error_to_diagnostic(&error),
262 }
263 }
264
265 async fn analyze_document(&self, uri: &Uri) {
267 let doc = match self.documents.get(uri) {
269 Some(doc) => doc,
270 None => return,
271 };
272
273 let text = doc.text();
275
276 let mut frontmatter_diagnostics = Vec::new();
278 let frontmatter_prefix_len;
279 {
280 use shape_runtime::frontmatter::{
281 FrontmatterDiagnosticSeverity, parse_frontmatter, parse_frontmatter_validated,
282 };
283 let (config, fm_diags, rest) = parse_frontmatter_validated(&text);
284 frontmatter_prefix_len = text.len().saturating_sub(rest.len());
285 for diag in fm_diags {
286 let severity = match diag.severity {
287 FrontmatterDiagnosticSeverity::Error => DiagnosticSeverity::ERROR,
288 FrontmatterDiagnosticSeverity::Warning => DiagnosticSeverity::WARNING,
289 };
290 let range = diag
291 .location
292 .map(|loc| Range {
293 start: Position {
294 line: loc.line,
295 character: loc.character,
296 },
297 end: Position {
298 line: loc.line,
299 character: loc.character + loc.length.max(1),
300 },
301 })
302 .unwrap_or_else(frontmatter_fallback_range);
303 frontmatter_diagnostics.push(Diagnostic {
304 range,
305 severity: Some(severity),
306 message: diag.message,
307 source: Some("shape".to_string()),
308 ..Default::default()
309 });
310 }
311
312 if config.is_some() {
314 if let (Some(project_root), Some(path)) =
315 (self.project_root.get(), uri.to_file_path())
316 {
317 if path.as_ref().starts_with(project_root) {
318 frontmatter_diagnostics.push(Diagnostic {
319 range: Range {
320 start: Position {
321 line: 0,
322 character: 0,
323 },
324 end: Position {
325 line: 0,
326 character: 3,
327 },
328 },
329 severity: Some(DiagnosticSeverity::ERROR),
330 message: "Frontmatter and shape.toml are mutually exclusive; use one configuration source.".to_string(),
331 source: Some("shape".to_string()),
332 ..Default::default()
333 });
334 }
335 }
336 }
337
338 if let (Some(frontmatter), Some(script_path)) =
340 (parse_frontmatter(&text).0, uri.to_file_path())
341 {
342 let path_ranges = frontmatter_extension_path_ranges(&text);
343 if let Some(script_dir) = script_path.as_ref().parent() {
344 for (index, extension) in frontmatter.extensions.into_iter().enumerate() {
345 let resolved = if extension.path.is_absolute() {
346 extension.path.clone()
347 } else {
348 script_dir.join(&extension.path)
349 };
350 if !resolved.exists() {
351 let range = path_ranges
352 .get(index)
353 .cloned()
354 .unwrap_or_else(frontmatter_fallback_range);
355 frontmatter_diagnostics.push(Diagnostic {
356 range,
357 severity: Some(DiagnosticSeverity::ERROR),
358 message: format!(
359 "Extension '{}' path does not exist: {}",
360 extension.name,
361 resolved.display()
362 ),
363 source: Some("shape".to_string()),
364 ..Default::default()
365 });
366 }
367 }
368 }
369 }
370 }
371
372 let parse_source = mask_leading_prefix_for_parse(&text, frontmatter_prefix_len);
373 let diagnostics = match parse_program(parse_source.as_ref()) {
374 Ok(program) => {
375 self.last_good_programs.insert(uri.clone(), program.clone());
377
378 let file_path = uri.to_file_path();
380 let foreign_startup_diagnostics = self
381 .foreign_lsp
382 .update_documents(
383 uri.as_str(),
384 &text,
385 &program.items,
386 file_path.as_deref(),
387 self.project_root.get().map(|p| p.as_path()),
388 )
389 .await;
390
391 let module_cache = self.documents.get_module_cache();
392 let mut diagnostics = crate::analysis::analyze_program_semantics(
393 &program,
394 &text,
395 uri.to_file_path().as_deref(),
396 Some(&module_cache),
397 self.project_root.get().map(|p| p.as_path()),
398 );
399 diagnostics.extend(crate::doc_diagnostics::validate_program_docs(
400 &program,
401 &text,
402 Some(&module_cache),
403 uri.to_file_path().as_deref(),
404 self.project_root.get().map(|p| p.as_path()),
405 ));
406 diagnostics.extend(foreign_startup_diagnostics);
407 diagnostics.extend(self.foreign_lsp.get_diagnostics(uri.as_str()).await);
408 diagnostics
409 }
410 Err(error) => {
411 let partial = shape_ast::parse_program_resilient(parse_source.as_ref());
413 let mut diagnostics = Vec::new();
414 let has_non_grammar_partial_error = partial
415 .errors
416 .iter()
417 .any(|e| !matches!(e.kind, ParseErrorKind::GrammarFailure));
418
419 if partial.errors.is_empty() || !has_non_grammar_partial_error {
422 diagnostics.extend(error_to_diagnostic(&error));
423 }
424
425 if has_non_grammar_partial_error {
427 for parse_error in &partial.errors {
428 if matches!(parse_error.kind, ParseErrorKind::GrammarFailure) {
429 continue;
430 }
431 let (start_line, start_col) = offset_to_line_col(&text, parse_error.span.0);
432 let (end_line, end_col) = offset_to_line_col(&text, parse_error.span.1);
433 diagnostics.push(Diagnostic {
434 range: Range {
435 start: Position {
436 line: start_line,
437 character: start_col,
438 },
439 end: Position {
440 line: end_line,
441 character: end_col,
442 },
443 },
444 severity: Some(DiagnosticSeverity::ERROR),
445 message: parse_error.message.clone(),
446 source: Some("shape".to_string()),
447 ..Default::default()
448 });
449 }
450 }
451
452 if !partial.items.is_empty() {
453 self.last_good_programs
455 .insert(uri.clone(), partial.into_program());
456 }
457
458 diagnostics
459 }
460 };
461
462 let mut all_diagnostics = frontmatter_diagnostics;
464 all_diagnostics.extend(diagnostics);
465
466 self.client
468 .publish_diagnostics(uri.clone(), all_diagnostics, None)
469 .await;
470 }
471}
472
473impl LanguageServer for ShapeLanguageServer {
474 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
475 self.client
477 .log_message(MessageType::INFO, "Shape Language Server initializing")
478 .await;
479
480 let mut workspace_folder: Option<std::path::PathBuf> = None;
482 if let Some(folders) = params.workspace_folders.as_ref() {
483 if let Some(folder) = folders.first() {
484 if let Some(folder_path) = folder.uri.to_file_path() {
485 workspace_folder = Some(folder_path.to_path_buf());
486 if let Some(project) = shape_runtime::project::find_project_root(&folder_path) {
487 self.client
488 .log_message(
489 MessageType::INFO,
490 format!(
491 "Detected project root: {} ({})",
492 project.config.project.name,
493 project.root_path.display()
494 ),
495 )
496 .await;
497 let _ = self.project_root.set(project.root_path);
498 }
499 }
500 }
501 }
502
503 if let Some(dir) = self
507 .project_root
508 .get()
509 .cloned()
510 .or_else(|| workspace_folder.clone())
511 {
512 self.foreign_lsp.set_workspace_dir(dir);
513 }
514
515 let workspace_hint = self
516 .project_root
517 .get()
518 .map(|path| path.as_path())
519 .or(workspace_folder.as_deref());
520 let configured_extensions = configured_extensions_from_lsp_value(
521 params.initialization_options.as_ref(),
522 workspace_hint,
523 );
524 if !configured_extensions.is_empty() {
525 self.client
526 .log_message(
527 MessageType::INFO,
528 format!(
529 "Configured {} always-load extension(s) from LSP initialization options",
530 configured_extensions.len()
531 ),
532 )
533 .await;
534 }
535 self.foreign_lsp
536 .set_configured_extensions(configured_extensions)
537 .await;
538
539 let initial_inlay_cfg =
541 InlayHintConfig::from_lsp_settings(params.initialization_options.as_ref());
542 if let Ok(mut guard) = self.inlay_hint_config.write() {
543 *guard = initial_inlay_cfg;
544 }
545
546 Ok(InitializeResult {
547 capabilities: ServerCapabilities {
548 text_document_sync: Some(TextDocumentSyncCapability::Kind(
550 TextDocumentSyncKind::FULL,
551 )),
552
553 completion_provider: Some(CompletionOptions {
560 resolve_provider: Some(true),
561 trigger_characters: Some(vec![
562 ".".to_string(),
563 "(".to_string(),
564 " ".to_string(),
565 "@".to_string(),
566 ":".to_string(),
567 ]),
568 work_done_progress_options: WorkDoneProgressOptions {
569 work_done_progress: None,
570 },
571 all_commit_characters: Some(vec![
572 ".".to_string(),
573 "(".to_string(),
574 ",".to_string(),
575 ";".to_string(),
576 ]),
577 completion_item: None,
578 }),
579
580 hover_provider: Some(HoverProviderCapability::Simple(true)),
582
583 signature_help_provider: Some(SignatureHelpOptions {
585 trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
586 retrigger_characters: None,
587 work_done_progress_options: WorkDoneProgressOptions {
588 work_done_progress: None,
589 },
590 }),
591
592 document_symbol_provider: Some(OneOf::Left(true)),
594
595 workspace_symbol_provider: Some(OneOf::Left(true)),
597
598 definition_provider: Some(OneOf::Left(true)),
600
601 declaration_provider: Some(DeclarationCapability::Simple(true)),
604
605 type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
608
609 implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
612
613 document_highlight_provider: Some(OneOf::Left(true)),
616
617 references_provider: Some(OneOf::Left(true)),
619
620 semantic_tokens_provider: Some(
624 SemanticTokensServerCapabilities::SemanticTokensOptions(
625 SemanticTokensOptions {
626 work_done_progress_options: WorkDoneProgressOptions {
627 work_done_progress: None,
628 },
629 legend: get_legend(),
630 range: Some(true),
631 full: Some(SemanticTokensFullOptions::Delta { delta: Some(true) }),
632 },
633 ),
634 ),
635
636 inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options(
640 InlayHintOptions {
641 work_done_progress_options: WorkDoneProgressOptions {
642 work_done_progress: None,
643 },
644 resolve_provider: Some(true),
645 },
646 ))),
647
648 code_action_provider: Some(CodeActionProviderCapability::Options(
650 CodeActionOptions {
651 code_action_kinds: Some(vec![
652 CodeActionKind::QUICKFIX,
653 CodeActionKind::REFACTOR,
654 CodeActionKind::REFACTOR_EXTRACT,
655 CodeActionKind::REFACTOR_REWRITE,
656 CodeActionKind::SOURCE,
657 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
658 CodeActionKind::SOURCE_FIX_ALL,
659 ]),
660 work_done_progress_options: WorkDoneProgressOptions {
661 work_done_progress: None,
662 },
663 resolve_provider: Some(false),
664 },
665 )),
666
667 document_formatting_provider: Some(OneOf::Left(true)),
669
670 document_range_formatting_provider: Some(OneOf::Left(true)),
672
673 document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions {
675 first_trigger_character: "}".to_string(),
676 more_trigger_character: Some(vec!["\n".to_string()]),
677 }),
678
679 rename_provider: Some(OneOf::Right(RenameOptions {
681 prepare_provider: Some(true),
682 work_done_progress_options: WorkDoneProgressOptions {
683 work_done_progress: None,
684 },
685 })),
686
687 code_lens_provider: Some(CodeLensOptions {
689 resolve_provider: Some(true),
690 }),
691
692 folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
694
695 call_hierarchy_provider: Some(CallHierarchyServerCapability::Simple(true)),
697
698 document_link_provider: Some(DocumentLinkOptions {
702 resolve_provider: Some(false),
703 work_done_progress_options: WorkDoneProgressOptions {
704 work_done_progress: None,
705 },
706 }),
707
708 execute_command_provider: Some(ExecuteCommandOptions {
718 commands: registered_commands(),
719 work_done_progress_options: WorkDoneProgressOptions {
720 work_done_progress: None,
721 },
722 }),
723
724 diagnostic_provider: Some(DiagnosticServerCapabilities::Options(
740 DiagnosticOptions {
741 identifier: Some("shape".to_string()),
742 inter_file_dependencies: true,
743 workspace_diagnostics: true,
744 work_done_progress_options: WorkDoneProgressOptions {
745 work_done_progress: None,
746 },
747 },
748 )),
749
750 workspace: Some(WorkspaceServerCapabilities {
758 workspace_folders: None,
759 file_operations: Some(WorkspaceFileOperationsServerCapabilities {
760 will_rename: Some(FileOperationRegistrationOptions {
761 filters: vec![FileOperationFilter {
762 scheme: Some("file".to_string()),
763 pattern: FileOperationPattern {
764 glob: "**/*.shape".to_string(),
765 matches: Some(FileOperationPatternKind::File),
766 options: None,
767 },
768 }],
769 }),
770 ..Default::default()
771 }),
772 }),
773
774 ..ServerCapabilities::default()
775 },
776 server_info: Some(ServerInfo {
777 name: "Shape Language Server".to_string(),
778 version: Some(env!("CARGO_PKG_VERSION").to_string()),
779 }),
780 ..InitializeResult::default()
781 })
782 }
783
784 async fn initialized(&self, _params: InitializedParams) {
785 self.client
786 .log_message(MessageType::INFO, "Shape Language Server initialized")
787 .await;
788 }
789
790 async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
791 let workspace_hint = self.project_root.get().map(|path| path.as_path());
792 let configured_extensions =
793 configured_extensions_from_lsp_value(Some(¶ms.settings), workspace_hint);
794 self.foreign_lsp
795 .set_configured_extensions(configured_extensions.clone())
796 .await;
797
798 let new_inlay_cfg = InlayHintConfig::from_lsp_settings(Some(¶ms.settings));
800 if let Ok(mut guard) = self.inlay_hint_config.write() {
801 *guard = new_inlay_cfg;
802 }
803 let _ = self.client.inlay_hint_refresh().await;
804
805 self.client
806 .log_message(
807 MessageType::INFO,
808 format!(
809 "Updated always-load extensions from configuration change ({} configured); inlay-hint config refreshed",
810 configured_extensions.len()
811 ),
812 )
813 .await;
814 }
815
816 async fn shutdown(&self) -> Result<()> {
817 self.client
818 .log_message(MessageType::INFO, "Shape Language Server shutting down")
819 .await;
820 self.foreign_lsp.shutdown().await;
822 Ok(())
823 }
824
825 async fn did_open(&self, params: DidOpenTextDocumentParams) {
826 let uri = params.text_document.uri;
827 let version = params.text_document.version;
828 let text = params.text_document.text;
829
830 self.client
831 .log_message(
832 MessageType::INFO,
833 format!("Document opened: {}", uri.as_str()),
834 )
835 .await;
836
837 self.documents.open(uri.clone(), version, text);
839
840 if Self::is_shape_toml(&uri) {
842 self.analyze_toml_document(&uri).await;
843 return;
844 }
845
846 self.analyze_document(&uri).await;
848 }
849
850 async fn did_change(&self, params: DidChangeTextDocumentParams) {
851 let uri = params.text_document.uri;
852 let version = params.text_document.version;
853
854 if let Some(change) = params.content_changes.into_iter().next() {
857 let text = change.text;
858 self.documents.update(&uri, version, text);
859
860 self.client
861 .log_message(
862 MessageType::INFO,
863 format!("Document changed: {} (version {})", uri.as_str(), version),
864 )
865 .await;
866
867 if Self::is_shape_toml(&uri) {
869 self.analyze_toml_document(&uri).await;
870 return;
871 }
872
873 self.analyze_document(&uri).await;
875 }
876 }
877
878 async fn did_close(&self, params: DidCloseTextDocumentParams) {
879 let uri = params.text_document.uri;
880
881 self.client
882 .log_message(
883 MessageType::INFO,
884 format!("Document closed: {}", uri.as_str()),
885 )
886 .await;
887
888 self.client
890 .publish_diagnostics(uri.clone(), vec![], None)
891 .await;
892
893 self.last_good_programs.remove(&uri);
895
896 self.semantic_tokens_cache.remove(&uri);
899
900 self.documents.close(&uri);
901 }
902
903 async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
904 let uri = params.text_document_position.text_document.uri;
905 let position = params.text_document_position.position;
906
907 self.client
908 .log_message(
909 MessageType::INFO,
910 format!(
911 "Completion requested at {}:{}:{}",
912 uri.as_str(),
913 position.line,
914 position.character
915 ),
916 )
917 .await;
918
919 if Self::is_shape_toml(&uri) {
921 let doc = match self.documents.get(&uri) {
922 Some(doc) => doc,
923 None => return Ok(None),
924 };
925 let text = doc.text();
926 let items = crate::toml_support::completions::get_toml_completions(&text, position);
927 return Ok(Some(CompletionResponse::Array(items)));
928 }
929
930 let doc = match self.documents.get(&uri) {
932 Some(doc) => doc,
933 None => return Ok(None),
934 };
935
936 let text = doc.text();
937
938 if is_position_in_frontmatter(&text, position) {
940 let items =
941 crate::toml_support::completions::get_frontmatter_completions(&text, position);
942 return Ok(Some(CompletionResponse::Array(items)));
943 }
944
945 if let Some(cached_program) = self.last_good_programs.get(&uri) {
947 if crate::foreign_lsp::is_position_in_foreign_block(
948 &cached_program.items,
949 &text,
950 position,
951 ) {
952 let completions = self
953 .foreign_lsp
954 .handle_completion(uri.as_str(), position, &cached_program.items, &text)
955 .await;
956 if let Some(items) = completions {
957 return Ok(Some(CompletionResponse::Array(items)));
958 }
959 }
961 }
962
963 let cached_symbols = self.documents.get_cached_symbols(&uri);
964 let cached_types = self.documents.get_cached_types(&uri);
965
966 let module_cache = self.documents.get_module_cache();
968 let file_path = uri.to_file_path();
969 let (completions, updated_symbols, updated_types) = get_completions_with_context(
970 &text,
971 position,
972 &cached_symbols,
973 &cached_types,
974 Some(&module_cache),
975 file_path.as_deref(),
976 self.project_root.get().map(|p| p.as_path()),
977 );
978
979 if let Some(symbols) = updated_symbols {
981 self.documents.update_cached_symbols(&uri, symbols);
982 }
983 if let Some(types) = updated_types {
984 self.documents.update_cached_types(&uri, types);
985 }
986
987 Ok(Some(CompletionResponse::Array(completions)))
988 }
989
990 async fn completion_resolve(&self, item: CompletionItem) -> Result<CompletionItem> {
1000 Ok(item)
1001 }
1002
1003 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
1004 let uri = params.text_document_position_params.text_document.uri;
1005 let position = params.text_document_position_params.position;
1006
1007 self.client
1008 .log_message(
1009 MessageType::INFO,
1010 format!(
1011 "Hover requested at {}:{}:{}",
1012 uri.as_str(),
1013 position.line,
1014 position.character
1015 ),
1016 )
1017 .await;
1018
1019 if Self::is_shape_toml(&uri) {
1021 let doc = match self.documents.get(&uri) {
1022 Some(doc) => doc,
1023 None => return Ok(None),
1024 };
1025 let text = doc.text();
1026 return Ok(crate::toml_support::hover::get_toml_hover(&text, position));
1027 }
1028
1029 let doc = match self.documents.get(&uri) {
1031 Some(doc) => doc,
1032 None => return Ok(None),
1033 };
1034
1035 let text = doc.text();
1036
1037 if let Some(cached_program) = self.last_good_programs.get(&uri) {
1039 if crate::foreign_lsp::is_position_in_foreign_block(
1040 &cached_program.items,
1041 &text,
1042 position,
1043 ) {
1044 let hover = self
1045 .foreign_lsp
1046 .handle_hover(uri.as_str(), position, &cached_program.items, &text)
1047 .await;
1048 if hover.is_some() {
1049 return Ok(hover);
1050 }
1051 }
1053 }
1054
1055 let module_cache = self.documents.get_module_cache();
1057 let file_path = uri.to_file_path();
1058 let cached = self.last_good_programs.get(&uri);
1059 let cached_ref = cached.as_ref().map(|r| r.value());
1060 let hover = get_hover(
1061 &text,
1062 position,
1063 Some(&module_cache),
1064 file_path.as_deref(),
1065 cached_ref,
1066 );
1067
1068 Ok(hover)
1069 }
1070
1071 async fn signature_help(&self, params: SignatureHelpParams) -> Result<Option<SignatureHelp>> {
1072 let uri = params.text_document_position_params.text_document.uri;
1073 let position = params.text_document_position_params.position;
1074
1075 let doc = match self.documents.get(&uri) {
1077 Some(doc) => doc,
1078 None => return Ok(None),
1079 };
1080
1081 let text = doc.text();
1082
1083 if let Some(cached_program) = self.last_good_programs.get(&uri) {
1084 if crate::foreign_lsp::is_position_in_foreign_block(
1085 &cached_program.items,
1086 &text,
1087 position,
1088 ) {
1089 let signature_help = self
1090 .foreign_lsp
1091 .handle_signature_help(uri.as_str(), position, &cached_program.items, &text)
1092 .await;
1093 if signature_help.is_some() {
1094 return Ok(signature_help);
1095 }
1096 }
1097 }
1098
1099 let sig_help = get_signature_help(&text, position);
1101
1102 Ok(sig_help)
1103 }
1104
1105 async fn document_symbol(
1106 &self,
1107 params: DocumentSymbolParams,
1108 ) -> Result<Option<DocumentSymbolResponse>> {
1109 let uri = params.text_document.uri;
1110
1111 let doc = match self.documents.get(&uri) {
1113 Some(doc) => doc,
1114 None => return Ok(None),
1115 };
1116
1117 let text = doc.text();
1118
1119 let symbols = get_document_symbols(&text);
1121
1122 Ok(symbols)
1123 }
1124
1125 async fn symbol(
1126 &self,
1127 params: WorkspaceSymbolParams,
1128 ) -> Result<Option<WorkspaceSymbolResponse>> {
1129 let query = params.query;
1130 let mut all_symbols = Vec::new();
1131 let mut visited: HashSet<std::path::PathBuf> = HashSet::new();
1132
1133 for uri in self.documents.all_uris() {
1138 if let Some(doc) = self.documents.get(&uri) {
1139 if let Some(path_cow) = uri.to_file_path() {
1140 visited.insert(path_cow.into_owned());
1141 }
1142 let text = doc.text();
1143 let symbols = get_workspace_symbols(&text, &uri, &query);
1144 all_symbols.extend(symbols);
1145 }
1146 }
1147
1148 if let Some(root) = self.project_root.get() {
1153 let module_cache = self.documents.get_module_cache();
1154 for path in module_cache.enumerate_workspace_shape_files(root.as_path()) {
1155 if !visited.insert(path.clone()) {
1156 continue;
1157 }
1158 let Some(file_uri) = Uri::from_file_path(&path) else {
1159 continue;
1160 };
1161 let Ok(text) = std::fs::read_to_string(&path) else {
1162 continue;
1163 };
1164 let symbols = get_workspace_symbols(&text, &file_uri, &query);
1165 all_symbols.extend(symbols);
1166 }
1167 }
1168
1169 if all_symbols.is_empty() {
1170 Ok(None)
1171 } else {
1172 Ok(Some(WorkspaceSymbolResponse::Flat(all_symbols)))
1173 }
1174 }
1175
1176 async fn goto_definition(
1177 &self,
1178 params: GotoDefinitionParams,
1179 ) -> Result<Option<GotoDefinitionResponse>> {
1180 let uri = params.text_document_position_params.text_document.uri;
1181 let position = params.text_document_position_params.position;
1182
1183 let doc = match self.documents.get(&uri) {
1185 Some(doc) => doc,
1186 None => return Ok(None),
1187 };
1188
1189 let text = doc.text();
1190
1191 if let Some(cached_program) = self.last_good_programs.get(&uri) {
1192 if crate::foreign_lsp::is_position_in_foreign_block(
1193 &cached_program.items,
1194 &text,
1195 position,
1196 ) {
1197 let definition = self
1198 .foreign_lsp
1199 .handle_definition(uri.as_str(), position, &cached_program.items, &text)
1200 .await;
1201 if definition.is_some() {
1202 return Ok(definition);
1203 }
1204 }
1205 }
1206
1207 let module_cache = self.documents.get_module_cache();
1209
1210 let mut annotation_discovery = AnnotationDiscovery::new();
1212 let parse_source = parser_source(&text);
1213 if let Ok(program) = parse_program(parse_source.as_ref()) {
1214 annotation_discovery.discover_from_program(&program);
1215 if let Some(file_path) = uri.to_file_path() {
1216 annotation_discovery.discover_from_imports_with_cache(
1217 &program,
1218 &file_path,
1219 &module_cache,
1220 self.project_root.get().map(|p| p.as_path()),
1221 );
1222 }
1223 }
1224
1225 let cached = self.last_good_programs.get(&uri);
1226 let cached_ref = cached.as_ref().map(|r| r.value());
1227 let definition = get_definition(
1228 &text,
1229 position,
1230 &uri,
1231 Some(&module_cache),
1232 Some(&annotation_discovery),
1233 cached_ref,
1234 );
1235
1236 Ok(definition)
1237 }
1238
1239 async fn references(&self, params: ReferenceParams) -> Result<Option<Vec<Location>>> {
1240 let uri = params.text_document_position.text_document.uri;
1241 let position = params.text_document_position.position;
1242
1243 let doc = match self.documents.get(&uri) {
1245 Some(doc) => doc,
1246 None => return Ok(None),
1247 };
1248
1249 let text = doc.text();
1250
1251 if let Some(cached_program) = self.last_good_programs.get(&uri) {
1252 if crate::foreign_lsp::is_position_in_foreign_block(
1253 &cached_program.items,
1254 &text,
1255 position,
1256 ) {
1257 let references = self
1258 .foreign_lsp
1259 .handle_references(uri.as_str(), position, &cached_program.items, &text)
1260 .await;
1261 if references.is_some() {
1262 return Ok(references);
1263 }
1264 }
1265 }
1266
1267 let cached = self.last_good_programs.get(&uri);
1272 let cached_ref = cached.as_ref().map(|r| r.value());
1273 let module_cache = self.documents.get_module_cache();
1274 let workspace_root = self.project_root.get().map(|p| p.as_path());
1275 let references = get_references_cross_file(
1276 &text,
1277 position,
1278 &uri,
1279 cached_ref,
1280 Some(&self.documents),
1281 Some(&module_cache),
1282 workspace_root,
1283 );
1284
1285 Ok(references)
1286 }
1287
1288 async fn goto_type_definition(
1294 &self,
1295 params: GotoTypeDefinitionParams,
1296 ) -> Result<Option<GotoTypeDefinitionResponse>> {
1297 let uri = params.text_document_position_params.text_document.uri;
1298 let position = params.text_document_position_params.position;
1299
1300 let doc = match self.documents.get(&uri) {
1301 Some(doc) => doc,
1302 None => return Ok(None),
1303 };
1304 let text = doc.text();
1305
1306 let module_cache = self.documents.get_module_cache();
1307 let cached = self.last_good_programs.get(&uri);
1308 let cached_ref = cached.as_ref().map(|r| r.value());
1309 let response = get_type_definition(&text, position, &uri, Some(&module_cache), cached_ref);
1310
1311 Ok(response)
1312 }
1313
1314 async fn goto_implementation(
1319 &self,
1320 params: GotoImplementationParams,
1321 ) -> Result<Option<GotoImplementationResponse>> {
1322 let uri = params.text_document_position_params.text_document.uri;
1323 let position = params.text_document_position_params.position;
1324
1325 let doc = match self.documents.get(&uri) {
1326 Some(doc) => doc,
1327 None => return Ok(None),
1328 };
1329 let text = doc.text();
1330
1331 let cached = self.last_good_programs.get(&uri);
1332 let cached_ref = cached.as_ref().map(|r| r.value());
1333 let locations = get_implementations(&text, position, &uri, cached_ref);
1334
1335 Ok(locations.map(GotoImplementationResponse::Array))
1336 }
1337
1338 async fn goto_declaration(
1342 &self,
1343 params: GotoDeclarationParams,
1344 ) -> Result<Option<GotoDeclarationResponse>> {
1345 let uri = params.text_document_position_params.text_document.uri;
1346 let position = params.text_document_position_params.position;
1347
1348 let doc = match self.documents.get(&uri) {
1349 Some(doc) => doc,
1350 None => return Ok(None),
1351 };
1352 let text = doc.text();
1353
1354 let module_cache = self.documents.get_module_cache();
1355
1356 let mut annotation_discovery = AnnotationDiscovery::new();
1359 let parse_source = parser_source(&text);
1360 if let Ok(program) = parse_program(parse_source.as_ref()) {
1361 annotation_discovery.discover_from_program(&program);
1362 if let Some(file_path) = uri.to_file_path() {
1363 annotation_discovery.discover_from_imports_with_cache(
1364 &program,
1365 &file_path,
1366 &module_cache,
1367 self.project_root.get().map(|p| p.as_path()),
1368 );
1369 }
1370 }
1371
1372 let cached = self.last_good_programs.get(&uri);
1373 let cached_ref = cached.as_ref().map(|r| r.value());
1374 let response = get_declaration(
1375 &text,
1376 position,
1377 &uri,
1378 Some(&module_cache),
1379 Some(&annotation_discovery),
1380 cached_ref,
1381 );
1382
1383 Ok(response)
1384 }
1385
1386 async fn document_highlight(
1392 &self,
1393 params: DocumentHighlightParams,
1394 ) -> Result<Option<Vec<DocumentHighlight>>> {
1395 let uri = params.text_document_position_params.text_document.uri;
1396 let position = params.text_document_position_params.position;
1397
1398 let doc = match self.documents.get(&uri) {
1399 Some(doc) => doc,
1400 None => return Ok(None),
1401 };
1402 let text = doc.text();
1403
1404 let cached = self.last_good_programs.get(&uri);
1405 let cached_ref = cached.as_ref().map(|r| r.value());
1406 let highlights = get_document_highlights(&text, position, cached_ref);
1407
1408 Ok(highlights)
1409 }
1410
1411 async fn semantic_tokens_full(
1412 &self,
1413 params: SemanticTokensParams,
1414 ) -> Result<Option<SemanticTokensResult>> {
1415 let uri = params.text_document.uri;
1416
1417 self.client
1418 .log_message(
1419 MessageType::INFO,
1420 format!("Semantic tokens requested for {}", uri.as_str()),
1421 )
1422 .await;
1423
1424 let Some(mut tokens) = self.collect_full_semantic_tokens(&uri).await else {
1425 return Ok(None);
1426 };
1427
1428 let result_id = self.next_semantic_tokens_result_id();
1432 tokens.result_id = Some(result_id.clone());
1433 self.semantic_tokens_cache.insert(
1434 uri,
1435 CachedSemanticTokens {
1436 result_id,
1437 data: tokens.data.clone(),
1438 },
1439 );
1440
1441 Ok(Some(SemanticTokensResult::Tokens(tokens)))
1442 }
1443
1444 async fn semantic_tokens_range(
1445 &self,
1446 params: SemanticTokensRangeParams,
1447 ) -> Result<Option<SemanticTokensRangeResult>> {
1448 let uri = params.text_document.uri;
1449 let range = params.range;
1450
1451 self.client
1452 .log_message(
1453 MessageType::INFO,
1454 format!(
1455 "Semantic tokens (range) requested for {} [{}:{} - {}:{}]",
1456 uri.as_str(),
1457 range.start.line,
1458 range.start.character,
1459 range.end.line,
1460 range.end.character,
1461 ),
1462 )
1463 .await;
1464
1465 let Some(tokens) = self.collect_full_semantic_tokens(&uri).await else {
1466 return Ok(None);
1467 };
1468
1469 let absolute = decode_semantic_tokens(&tokens.data);
1474 let filtered: Vec<AbsoluteSemanticToken> = absolute
1475 .into_iter()
1476 .filter(|token| position_in_range(token.line, token.start_char, &range))
1477 .collect();
1478
1479 let result = SemanticTokens {
1480 result_id: None,
1483 data: encode_semantic_tokens(&filtered),
1484 };
1485 Ok(Some(SemanticTokensRangeResult::Tokens(result)))
1486 }
1487
1488 async fn semantic_tokens_full_delta(
1489 &self,
1490 params: SemanticTokensDeltaParams,
1491 ) -> Result<Option<SemanticTokensFullDeltaResult>> {
1492 let uri = params.text_document.uri;
1493 let previous_result_id = params.previous_result_id;
1494
1495 self.client
1496 .log_message(
1497 MessageType::INFO,
1498 format!(
1499 "Semantic tokens (delta) requested for {} (previous_result_id={})",
1500 uri.as_str(),
1501 previous_result_id,
1502 ),
1503 )
1504 .await;
1505
1506 let Some(mut tokens) = self.collect_full_semantic_tokens(&uri).await else {
1507 return Ok(None);
1508 };
1509
1510 let new_result_id = self.next_semantic_tokens_result_id();
1511
1512 let cached = self
1515 .semantic_tokens_cache
1516 .get(&uri)
1517 .map(|entry| entry.clone());
1518 let edits = match cached {
1519 Some(cached) if cached.result_id == previous_result_id => {
1520 Some(compute_semantic_token_edits(&cached.data, &tokens.data))
1521 }
1522 _ => None,
1523 };
1524
1525 self.semantic_tokens_cache.insert(
1528 uri,
1529 CachedSemanticTokens {
1530 result_id: new_result_id.clone(),
1531 data: tokens.data.clone(),
1532 },
1533 );
1534
1535 match edits {
1536 Some(edits) => Ok(Some(SemanticTokensFullDeltaResult::TokensDelta(
1537 SemanticTokensDelta {
1538 result_id: Some(new_result_id),
1539 edits,
1540 },
1541 ))),
1542 None => {
1543 tokens.result_id = Some(new_result_id);
1544 Ok(Some(SemanticTokensFullDeltaResult::Tokens(tokens)))
1545 }
1546 }
1547 }
1548
1549 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
1550 let uri = params.text_document.uri;
1551 let range = params.range;
1552
1553 let doc = match self.documents.get(&uri) {
1555 Some(doc) => doc,
1556 None => return Ok(None),
1557 };
1558
1559 let text = doc.text();
1560
1561 let config = self
1564 .inlay_hint_config
1565 .read()
1566 .map(|guard| guard.clone())
1567 .unwrap_or_default();
1568 let cached = self.last_good_programs.get(&uri);
1569 let cached_ref = cached.as_ref().map(|r| r.value());
1570 let file_path = uri.to_file_path();
1571 let hints = get_inlay_hints_with_context(
1572 &text,
1573 range,
1574 &config,
1575 cached_ref,
1576 file_path.as_deref(),
1577 self.project_root.get().map(|p| p.as_path()),
1578 );
1579
1580 if hints.is_empty() {
1581 Ok(None)
1582 } else {
1583 Ok(Some(hints))
1584 }
1585 }
1586
1587 async fn inlay_hint_resolve(&self, mut hint: InlayHint) -> Result<InlayHint> {
1592 use tower_lsp_server::ls_types::InlayHintTooltip;
1593
1594 if hint.tooltip.is_some() {
1595 return Ok(hint);
1596 }
1597
1598 let kind = hint
1601 .data
1602 .as_ref()
1603 .and_then(|v| v.get("kind"))
1604 .and_then(|v| v.as_str());
1605
1606 let tooltip = match kind {
1607 Some("chain") => Some(
1608 "Inferred type of the intermediate method-chain step (W2.4 / 1.27)."
1609 .to_string(),
1610 ),
1611 Some("binding-kind") => Some(
1612 "LSP-side approximation of BindingStorageClass (ADR-006 §2). \
1613 The compiler at crates/shape-vm/src/type_tracking.rs:286 is authoritative."
1614 .to_string(),
1615 ),
1616 _ => None,
1617 };
1618
1619 if let Some(text) = tooltip {
1620 hint.tooltip = Some(InlayHintTooltip::String(text));
1621 }
1622
1623 Ok(hint)
1624 }
1625
1626 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
1627 let uri = params.text_document.uri;
1628 let range = params.range;
1629 let diagnostics = params.context.diagnostics;
1630
1631 let doc = match self.documents.get(&uri) {
1633 Some(doc) => doc,
1634 None => return Ok(None),
1635 };
1636
1637 let text = doc.text();
1638
1639 let module_cache = self.documents.get_module_cache();
1641 let actions = get_code_actions(
1642 &text,
1643 &uri,
1644 range,
1645 &diagnostics,
1646 Some(&module_cache),
1647 params.context.only.as_deref(),
1648 );
1649
1650 if actions.is_empty() {
1651 Ok(None)
1652 } else {
1653 Ok(Some(actions))
1654 }
1655 }
1656
1657 async fn diagnostic(
1668 &self,
1669 params: DocumentDiagnosticParams,
1670 ) -> Result<DocumentDiagnosticReportResult> {
1671 let uri = params.text_document.uri;
1672
1673 let items = self.compute_pull_diagnostics(&uri).await;
1674
1675 Ok(DocumentDiagnosticReportResult::Report(
1676 DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
1677 related_documents: None,
1678 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1679 result_id: None,
1680 items,
1681 },
1682 }),
1683 ))
1684 }
1685
1686 async fn workspace_diagnostic(
1698 &self,
1699 _params: WorkspaceDiagnosticParams,
1700 ) -> Result<WorkspaceDiagnosticReportResult> {
1701 let mut items: Vec<WorkspaceDocumentDiagnosticReport> = Vec::new();
1702 for uri in self.documents.all_uris() {
1703 let diags = self.compute_pull_diagnostics(&uri).await;
1704 items.push(WorkspaceDocumentDiagnosticReport::Full(
1705 WorkspaceFullDocumentDiagnosticReport {
1706 uri,
1707 version: None,
1708 full_document_diagnostic_report: FullDocumentDiagnosticReport {
1709 result_id: None,
1710 items: diags,
1711 },
1712 },
1713 ));
1714 }
1715 Ok(WorkspaceDiagnosticReportResult::Report(
1716 WorkspaceDiagnosticReport { items },
1717 ))
1718 }
1719
1720 async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
1721 let uri = params.text_document.uri;
1722
1723 let doc = match self.documents.get(&uri) {
1725 Some(doc) => doc,
1726 None => return Ok(None),
1727 };
1728
1729 let text = doc.text();
1730
1731 let edits = format_document(&text, ¶ms.options);
1733
1734 if edits.is_empty() {
1735 Ok(None)
1736 } else {
1737 Ok(Some(edits))
1738 }
1739 }
1740
1741 async fn range_formatting(
1742 &self,
1743 params: DocumentRangeFormattingParams,
1744 ) -> Result<Option<Vec<TextEdit>>> {
1745 let uri = params.text_document.uri;
1746 let range = params.range;
1747
1748 let doc = match self.documents.get(&uri) {
1750 Some(doc) => doc,
1751 None => return Ok(None),
1752 };
1753
1754 let text = doc.text();
1755
1756 let edits = format_range(&text, range, ¶ms.options);
1758
1759 if edits.is_empty() {
1760 Ok(None)
1761 } else {
1762 Ok(Some(edits))
1763 }
1764 }
1765
1766 async fn on_type_formatting(
1767 &self,
1768 params: DocumentOnTypeFormattingParams,
1769 ) -> Result<Option<Vec<TextEdit>>> {
1770 let uri = params.text_document_position.text_document.uri;
1771 let position = params.text_document_position.position;
1772
1773 let doc = match self.documents.get(&uri) {
1774 Some(doc) => doc,
1775 None => return Ok(None),
1776 };
1777
1778 let text = doc.text();
1779 let edits = format_on_type(&text, position, ¶ms.ch, ¶ms.options);
1780
1781 if edits.is_empty() {
1782 Ok(None)
1783 } else {
1784 Ok(Some(edits))
1785 }
1786 }
1787
1788 async fn prepare_rename(
1789 &self,
1790 params: TextDocumentPositionParams,
1791 ) -> Result<Option<PrepareRenameResponse>> {
1792 let uri = params.text_document.uri;
1793 let position = params.position;
1794
1795 let doc = match self.documents.get(&uri) {
1797 Some(doc) => doc,
1798 None => return Ok(None),
1799 };
1800
1801 let text = doc.text();
1802
1803 let response = prepare_rename(&text, position);
1805
1806 Ok(response)
1807 }
1808
1809 async fn rename(&self, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
1810 let uri = params.text_document_position.text_document.uri;
1811 let position = params.text_document_position.position;
1812 let new_name = params.new_name;
1813
1814 let doc = match self.documents.get(&uri) {
1816 Some(doc) => doc,
1817 None => return Ok(None),
1818 };
1819
1820 let text = doc.text();
1821
1822 let cached = self.last_good_programs.get(&uri);
1827 let cached_ref = cached.as_ref().map(|r| r.value());
1828 let module_cache = self.documents.get_module_cache();
1829 let workspace_root = self.project_root.get().map(|p| p.as_path());
1830 let edit = rename_cross_file(
1831 &text,
1832 &uri,
1833 position,
1834 &new_name,
1835 cached_ref,
1836 Some(&self.documents),
1837 Some(&module_cache),
1838 workspace_root,
1839 );
1840
1841 Ok(edit)
1842 }
1843
1844 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
1845 let uri = params.text_document.uri;
1846
1847 let doc = match self.documents.get(&uri) {
1849 Some(doc) => doc,
1850 None => return Ok(None),
1851 };
1852
1853 let text = doc.text();
1854
1855 let lenses = get_code_lenses(&text, &uri);
1857
1858 if lenses.is_empty() {
1859 Ok(None)
1860 } else {
1861 Ok(Some(lenses))
1862 }
1863 }
1864
1865 async fn code_lens_resolve(&self, lens: CodeLens) -> Result<CodeLens> {
1866 Ok(resolve_code_lens(lens))
1867 }
1868
1869 async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
1870 let uri = params.text_document.uri;
1871
1872 let doc = match self.documents.get(&uri) {
1873 Some(doc) => doc,
1874 None => return Ok(None),
1875 };
1876
1877 let text = doc.text();
1878 let parse_source = parser_source(&text);
1879 let program = match parse_program(parse_source.as_ref()) {
1880 Ok(p) => p,
1881 Err(_) => {
1882 match self.last_good_programs.get(&uri) {
1884 Some(cached) => cached.value().clone(),
1885 None => return Ok(None),
1886 }
1887 }
1888 };
1889
1890 let ranges = get_folding_ranges(&text, &program);
1891 if ranges.is_empty() {
1892 Ok(None)
1893 } else {
1894 Ok(Some(ranges))
1895 }
1896 }
1897
1898 async fn document_link(
1899 &self,
1900 params: DocumentLinkParams,
1901 ) -> Result<Option<Vec<DocumentLink>>> {
1902 let uri = params.text_document.uri;
1903
1904 let doc = match self.documents.get(&uri) {
1905 Some(doc) => doc,
1906 None => return Ok(None),
1907 };
1908
1909 let text = doc.text();
1910 let parse_source = parser_source(&text);
1911 let program = match parse_program(parse_source.as_ref()) {
1912 Ok(program) => program,
1913 Err(_) => match self.last_good_programs.get(&uri) {
1914 Some(cached) => cached.value().clone(),
1915 None => return Ok(None),
1916 },
1917 };
1918
1919 let module_cache = self.documents.get_module_cache();
1920 let file_path = uri.to_file_path();
1921 let links = crate::doc_links::collect_document_links(
1922 &program,
1923 &text,
1924 Some(&module_cache),
1925 file_path.as_deref(),
1926 self.project_root.get().map(|p| p.as_path()),
1927 );
1928
1929 if links.is_empty() {
1930 Ok(None)
1931 } else {
1932 Ok(Some(links))
1933 }
1934 }
1935
1936 async fn prepare_call_hierarchy(
1937 &self,
1938 params: CallHierarchyPrepareParams,
1939 ) -> Result<Option<Vec<CallHierarchyItem>>> {
1940 let uri = params.text_document_position_params.text_document.uri;
1941 let position = params.text_document_position_params.position;
1942
1943 let doc = match self.documents.get(&uri) {
1944 Some(doc) => doc,
1945 None => return Ok(None),
1946 };
1947
1948 let text = doc.text();
1949 Ok(ch_prepare(&text, position, &uri))
1950 }
1951
1952 async fn incoming_calls(
1953 &self,
1954 params: CallHierarchyIncomingCallsParams,
1955 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1956 let uri = ¶ms.item.uri;
1957
1958 let doc = match self.documents.get(uri) {
1959 Some(doc) => doc,
1960 None => return Ok(None),
1961 };
1962
1963 let text = doc.text();
1964 let results = ch_incoming(&text, ¶ms.item, uri);
1965 if results.is_empty() {
1966 Ok(None)
1967 } else {
1968 Ok(Some(results))
1969 }
1970 }
1971
1972 async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
1981 let module_cache = self.documents.get_module_cache();
1982 let mut invalidated = 0usize;
1983 for event in ¶ms.changes {
1984 let Some(path_cow) = event.uri.to_file_path() else {
1987 continue;
1988 };
1989 let path = path_cow.into_owned();
1990 if path.extension().and_then(|e| e.to_str()) != Some("shape") {
1991 continue;
1992 }
1993 module_cache.invalidate(&path);
1994 if matches!(event.typ, FileChangeType::DELETED | FileChangeType::CHANGED) {
1997 self.last_good_programs.remove(&event.uri);
1998 }
1999 invalidated += 1;
2000 }
2001 if invalidated > 0 {
2002 self.client
2003 .log_message(
2004 MessageType::INFO,
2005 format!(
2006 "didChangeWatchedFiles: invalidated {} module-cache entr{}",
2007 invalidated,
2008 if invalidated == 1 { "y" } else { "ies" }
2009 ),
2010 )
2011 .await;
2012 }
2013 }
2014
2015 async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<LSPAny>> {
2029 if !registered_commands().contains(¶ms.command) {
2030 self.client
2031 .log_message(
2032 MessageType::WARNING,
2033 format!(
2034 "executeCommand: unknown command {:?}; declared commands: {:?}",
2035 params.command,
2036 registered_commands()
2037 ),
2038 )
2039 .await;
2040 return Err(tower_lsp_server::jsonrpc::Error::method_not_found());
2041 }
2042 self.client
2043 .log_message(
2044 MessageType::INFO,
2045 format!(
2046 "executeCommand: {:?} acknowledged ({} arg{})",
2047 params.command,
2048 params.arguments.len(),
2049 if params.arguments.len() == 1 { "" } else { "s" }
2050 ),
2051 )
2052 .await;
2053 Ok(None)
2054 }
2055
2056 async fn will_rename_files(
2070 &self,
2071 params: RenameFilesParams,
2072 ) -> Result<Option<WorkspaceEdit>> {
2073 let Some(workspace_root) = self.project_root.get() else {
2074 return Ok(None);
2075 };
2076 let module_cache = self.documents.get_module_cache();
2077
2078 let mut renames: Vec<(String, String)> = Vec::new();
2080 for rename in ¶ms.files {
2081 let Ok(old_uri) = rename.old_uri.parse::<Uri>() else {
2082 continue;
2083 };
2084 let Ok(new_uri) = rename.new_uri.parse::<Uri>() else {
2085 continue;
2086 };
2087 let Some(old_path) = old_uri.to_file_path() else {
2088 continue;
2089 };
2090 let Some(new_path) = new_uri.to_file_path() else {
2091 continue;
2092 };
2093 let Some(old_mod) = crate::module_cache::path_to_module_path(
2094 &old_path,
2095 workspace_root.as_path(),
2096 ) else {
2097 continue;
2098 };
2099 let Some(new_mod) = crate::module_cache::path_to_module_path(
2100 &new_path,
2101 workspace_root.as_path(),
2102 ) else {
2103 continue;
2104 };
2105 if old_mod == new_mod {
2106 continue;
2107 }
2108 renames.push((old_mod, new_mod));
2109 }
2110 if renames.is_empty() {
2111 return Ok(None);
2112 }
2113
2114 let mut changes: std::collections::HashMap<Uri, Vec<TextEdit>> =
2115 std::collections::HashMap::new();
2116 let mut visited: HashSet<std::path::PathBuf> = HashSet::new();
2117
2118 for uri in self.documents.all_uris() {
2120 let Some(doc) = self.documents.get(&uri) else {
2121 continue;
2122 };
2123 if let Some(p) = uri.to_file_path() {
2124 visited.insert(p.into_owned());
2125 }
2126 let text = doc.text();
2127 let edits = collect_import_rewrites(&text, &renames);
2128 if !edits.is_empty() {
2129 changes.entry(uri).or_insert_with(Vec::new).extend(edits);
2130 }
2131 }
2132
2133 for path in module_cache.enumerate_workspace_shape_files(workspace_root.as_path()) {
2135 if !visited.insert(path.clone()) {
2136 continue;
2137 }
2138 let Some(uri) = Uri::from_file_path(&path) else {
2139 continue;
2140 };
2141 let Ok(text) = std::fs::read_to_string(&path) else {
2142 continue;
2143 };
2144 let edits = collect_import_rewrites(&text, &renames);
2145 if !edits.is_empty() {
2146 changes.entry(uri).or_insert_with(Vec::new).extend(edits);
2147 }
2148 }
2149
2150 if changes.is_empty() {
2151 Ok(None)
2152 } else {
2153 Ok(Some(WorkspaceEdit {
2154 changes: Some(changes),
2155 document_changes: None,
2156 change_annotations: None,
2157 }))
2158 }
2159 }
2160
2161 async fn outgoing_calls(
2162 &self,
2163 params: CallHierarchyOutgoingCallsParams,
2164 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
2165 let uri = ¶ms.item.uri;
2166
2167 let doc = match self.documents.get(uri) {
2168 Some(doc) => doc,
2169 None => return Ok(None),
2170 };
2171
2172 let text = doc.text();
2173 let results = ch_outgoing(&text, ¶ms.item, uri);
2174 if results.is_empty() {
2175 Ok(None)
2176 } else {
2177 Ok(Some(results))
2178 }
2179 }
2180}
2181
2182fn registered_commands() -> Vec<String> {
2187 vec![
2188 "shape.findReferences".to_string(),
2189 "shape.findImplementations".to_string(),
2190 "shape.runTests".to_string(),
2191 "shape.debugTests".to_string(),
2192 "shape.showAnnotation".to_string(),
2193 "shape.showTraitMethod".to_string(),
2194 ]
2195}
2196
2197fn collect_import_rewrites(text: &str, renames: &[(String, String)]) -> Vec<TextEdit> {
2207 use crate::util::offset_to_line_col;
2208 let mut edits = Vec::new();
2209 let mut offset = 0usize;
2210 for line in text.split_inclusive('\n') {
2211 let trimmed_offset = offset
2212 + (line.len() - line.trim_start().len());
2213 let body = line.trim_start();
2214 if let Some(rest) = body.strip_prefix("from ") {
2216 let path_start_in_body = "from ".len() + rest.len() - rest.trim_start().len();
2218 let path_start = trimmed_offset + path_start_in_body;
2219 let after_from = rest.trim_start();
2220 let mut end = 0usize;
2224 let bytes = after_from.as_bytes();
2225 while end < bytes.len() {
2226 let b = bytes[end];
2227 if b.is_ascii_alphanumeric() || b == b'_' {
2228 end += 1;
2229 } else if end + 1 < bytes.len() && b == b':' && bytes[end + 1] == b':' {
2230 end += 2;
2231 } else {
2232 break;
2233 }
2234 }
2235 if end > 0 {
2236 let actual_path = &after_from[..end];
2237 for (old, new) in renames {
2238 if actual_path == old {
2239 let path_end = path_start + end;
2240 let (sl, sc) = offset_to_line_col(text, path_start);
2241 let (el, ec) = offset_to_line_col(text, path_end);
2242 edits.push(TextEdit {
2243 range: Range {
2244 start: Position {
2245 line: sl,
2246 character: sc,
2247 },
2248 end: Position {
2249 line: el,
2250 character: ec,
2251 },
2252 },
2253 new_text: new.clone(),
2254 });
2255 break;
2256 }
2257 }
2258 }
2259 }
2260 offset += line.len();
2261 }
2262 edits
2263}
2264
2265fn frontmatter_fallback_range() -> Range {
2266 Range {
2267 start: Position {
2268 line: 0,
2269 character: 0,
2270 },
2271 end: Position {
2272 line: 0,
2273 character: 3,
2274 },
2275 }
2276}
2277
2278#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2279struct AbsoluteSemanticToken {
2280 line: u32,
2281 start_char: u32,
2282 length: u32,
2283 token_type: u32,
2284 modifiers: u32,
2285}
2286
2287fn decode_semantic_tokens(tokens: &[SemanticToken]) -> Vec<AbsoluteSemanticToken> {
2288 let mut decoded = Vec::with_capacity(tokens.len());
2289 let mut line = 0u32;
2290 let mut col = 0u32;
2291
2292 for token in tokens {
2293 line += token.delta_line;
2294 if token.delta_line == 0 {
2295 col += token.delta_start;
2296 } else {
2297 col = token.delta_start;
2298 }
2299 decoded.push(AbsoluteSemanticToken {
2300 line,
2301 start_char: col,
2302 length: token.length,
2303 token_type: token.token_type,
2304 modifiers: token.token_modifiers_bitset,
2305 });
2306 }
2307
2308 decoded
2309}
2310
2311fn encode_semantic_tokens(tokens: &[AbsoluteSemanticToken]) -> Vec<SemanticToken> {
2312 let mut encoded = Vec::with_capacity(tokens.len());
2313 let mut prev_line = 0u32;
2314 let mut prev_start = 0u32;
2315
2316 for token in tokens {
2317 let delta_line = token.line.saturating_sub(prev_line);
2318 let delta_start = if delta_line == 0 {
2319 token.start_char.saturating_sub(prev_start)
2320 } else {
2321 token.start_char
2322 };
2323 encoded.push(SemanticToken {
2324 delta_line,
2325 delta_start,
2326 length: token.length,
2327 token_type: token.token_type,
2328 token_modifiers_bitset: token.modifiers,
2329 });
2330 prev_line = token.line;
2331 prev_start = token.start_char;
2332 }
2333
2334 encoded
2335}
2336
2337fn position_in_range(line: u32, character: u32, range: &Range) -> bool {
2343 let start = (range.start.line, range.start.character);
2344 let end = (range.end.line, range.end.character);
2345 let pos = (line, character);
2346 pos >= start && pos < end
2347}
2348
2349fn compute_semantic_token_edits(
2362 old: &[SemanticToken],
2363 new: &[SemanticToken],
2364) -> Vec<SemanticTokensEdit> {
2365 let mut prefix = 0usize;
2367 let max_prefix = old.len().min(new.len());
2368 while prefix < max_prefix && semantic_tokens_eq(&old[prefix], &new[prefix]) {
2369 prefix += 1;
2370 }
2371
2372 let mut suffix = 0usize;
2374 let max_suffix = old.len().min(new.len()) - prefix;
2375 while suffix < max_suffix
2376 && semantic_tokens_eq(
2377 &old[old.len() - 1 - suffix],
2378 &new[new.len() - 1 - suffix],
2379 )
2380 {
2381 suffix += 1;
2382 }
2383
2384 let old_replace_start = prefix;
2385 let old_replace_end = old.len() - suffix;
2386 let new_replace_start = prefix;
2387 let new_replace_end = new.len() - suffix;
2388
2389 if old_replace_start == old_replace_end && new_replace_start == new_replace_end {
2390 return Vec::new();
2392 }
2393
2394 let start = (old_replace_start * 5) as u32;
2396 let delete_count = ((old_replace_end - old_replace_start) * 5) as u32;
2397 let replacement: Vec<SemanticToken> =
2398 new[new_replace_start..new_replace_end].to_vec();
2399 let data = if replacement.is_empty() {
2400 None
2401 } else {
2402 Some(replacement)
2403 };
2404
2405 vec![SemanticTokensEdit {
2406 start,
2407 delete_count,
2408 data,
2409 }]
2410}
2411
2412fn semantic_tokens_eq(a: &SemanticToken, b: &SemanticToken) -> bool {
2413 a.delta_line == b.delta_line
2414 && a.delta_start == b.delta_start
2415 && a.length == b.length
2416 && a.token_type == b.token_type
2417 && a.token_modifiers_bitset == b.token_modifiers_bitset
2418}
2419
2420fn frontmatter_extension_path_ranges(source: &str) -> Vec<Range> {
2421 let lines: Vec<&str> = source.split('\n').collect();
2422 if lines.is_empty() {
2423 return Vec::new();
2424 }
2425
2426 let delimiter_lines: Vec<usize> = lines
2427 .iter()
2428 .enumerate()
2429 .filter_map(|(idx, line)| {
2430 let trimmed = line.trim_end_matches('\r').trim();
2431 if trimmed == "---" { Some(idx) } else { None }
2432 })
2433 .take(2)
2434 .collect();
2435
2436 if delimiter_lines.len() < 2 {
2437 return Vec::new();
2438 }
2439
2440 let start = delimiter_lines[0] + 1;
2441 let end = delimiter_lines[1];
2442 let mut in_extensions = false;
2443 let mut ranges = Vec::new();
2444
2445 for (line_idx, raw_line) in lines.iter().enumerate().take(end).skip(start) {
2446 let trimmed = raw_line.trim();
2447 if trimmed.starts_with("[[extensions]]") {
2448 in_extensions = true;
2449 continue;
2450 }
2451
2452 if trimmed.starts_with("[[") || (trimmed.starts_with('[') && trimmed.ends_with(']')) {
2453 in_extensions = false;
2454 continue;
2455 }
2456
2457 if !in_extensions {
2458 continue;
2459 }
2460
2461 let Some(eq_pos) = raw_line.find('=') else {
2462 continue;
2463 };
2464 let key = raw_line[..eq_pos].trim();
2465 if key != "path" {
2466 continue;
2467 }
2468
2469 let key_start = raw_line.find("path").unwrap_or_else(|| {
2470 raw_line[..eq_pos]
2471 .find(|c: char| !c.is_whitespace())
2472 .unwrap_or(0)
2473 });
2474 let line_len = raw_line.trim_end_matches('\r').len();
2475 let end_char = line_len.max(key_start + 4);
2476
2477 ranges.push(Range {
2478 start: Position {
2479 line: line_idx as u32,
2480 character: key_start as u32,
2481 },
2482 end: Position {
2483 line: line_idx as u32,
2484 character: end_char as u32,
2485 },
2486 });
2487 }
2488
2489 ranges
2490}
2491
2492fn is_position_in_frontmatter(source: &str, position: Position) -> bool {
2493 if source.starts_with("#!") && position.line == 0 {
2494 return false;
2495 }
2496
2497 let (_, _, rest) = shape_runtime::frontmatter::parse_frontmatter_validated(source);
2498 let prefix_len = source.len().saturating_sub(rest.len());
2499 if prefix_len == 0 {
2500 return false;
2501 }
2502
2503 position_to_offset(source, position)
2504 .map(|offset| offset < prefix_len)
2505 .unwrap_or(false)
2506}
2507
2508fn configured_extensions_from_lsp_value(
2509 options: Option<&serde_json::Value>,
2510 workspace_root: Option<&std::path::Path>,
2511) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
2512 let mut specs = collect_configured_extensions_from_options(options, workspace_root);
2513
2514 collect_global_extensions(&mut specs);
2516
2517 dedup_extension_specs(specs)
2518}
2519
2520fn collect_configured_extensions_from_options(
2522 options: Option<&serde_json::Value>,
2523 workspace_root: Option<&std::path::Path>,
2524) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
2525 let mut specs = Vec::new();
2526
2527 if let Some(options) = options {
2528 collect_configured_extensions_from_array(
2529 options.get("alwaysLoadExtensions"),
2530 workspace_root,
2531 &mut specs,
2532 );
2533 collect_configured_extensions_from_array(
2534 options.get("always_load_extensions"),
2535 workspace_root,
2536 &mut specs,
2537 );
2538
2539 if let Some(shape) = options.get("shape") {
2540 collect_configured_extensions_from_array(
2541 shape.get("alwaysLoadExtensions"),
2542 workspace_root,
2543 &mut specs,
2544 );
2545 collect_configured_extensions_from_array(
2546 shape.get("always_load_extensions"),
2547 workspace_root,
2548 &mut specs,
2549 );
2550 }
2551 }
2552
2553 specs
2554}
2555
2556fn dedup_extension_specs(
2557 specs: Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
2558) -> Vec<crate::foreign_lsp::ConfiguredExtensionSpec> {
2559 let mut seen = HashSet::new();
2560 specs
2561 .into_iter()
2562 .filter(|spec| {
2563 let key = format!(
2564 "{}|{}|{}",
2565 spec.name,
2566 spec.path.display(),
2567 serde_json::to_string(&spec.config).unwrap_or_default()
2568 );
2569 seen.insert(key)
2570 })
2571 .collect()
2572}
2573
2574fn collect_global_extensions(out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>) {
2575 let Some(home) = dirs::home_dir() else {
2576 return;
2577 };
2578 let ext_dir = home.join(".shape").join("extensions");
2579 if !ext_dir.is_dir() {
2580 return;
2581 }
2582 let Ok(entries) = std::fs::read_dir(&ext_dir) else {
2583 return;
2584 };
2585 for entry in entries.flatten() {
2586 let path = entry.path();
2587 let is_lib = path
2588 .extension()
2589 .and_then(|e| e.to_str())
2590 .map(|ext| ext == "so" || ext == "dylib" || ext == "dll")
2591 .unwrap_or(false);
2592 if !is_lib {
2593 continue;
2594 }
2595 let name = path
2596 .file_stem()
2597 .and_then(|s| s.to_str())
2598 .map(|s| {
2599 s.strip_prefix("libshape_ext_")
2600 .or_else(|| s.strip_prefix("shape_ext_"))
2601 .unwrap_or(s)
2602 .to_string()
2603 })
2604 .unwrap_or_else(|| "extension".to_string());
2605 out.push(crate::foreign_lsp::ConfiguredExtensionSpec {
2606 name,
2607 path,
2608 config: serde_json::json!({}),
2609 });
2610 }
2611}
2612
2613fn collect_configured_extensions_from_array(
2614 value: Option<&serde_json::Value>,
2615 workspace_root: Option<&std::path::Path>,
2616 out: &mut Vec<crate::foreign_lsp::ConfiguredExtensionSpec>,
2617) {
2618 let Some(items) = value.and_then(|v| v.as_array()) else {
2619 return;
2620 };
2621 for item in items {
2622 if let Some(spec) = parse_configured_extension_item(item, workspace_root) {
2623 out.push(spec);
2624 }
2625 }
2626}
2627
2628fn parse_configured_extension_item(
2629 item: &serde_json::Value,
2630 workspace_root: Option<&std::path::Path>,
2631) -> Option<crate::foreign_lsp::ConfiguredExtensionSpec> {
2632 let (name, path, config) = if let Some(path) = item.as_str() {
2633 let path_buf = resolve_configured_extension_path(path, workspace_root);
2634 (
2635 configured_extension_name_from_path(&path_buf),
2636 path_buf,
2637 serde_json::json!({}),
2638 )
2639 } else if let Some(obj) = item.as_object() {
2640 let path_str = obj.get("path")?.as_str()?;
2641 let path_buf = resolve_configured_extension_path(path_str, workspace_root);
2642 let name = obj
2643 .get("name")
2644 .and_then(|value| value.as_str())
2645 .map(String::from)
2646 .unwrap_or_else(|| configured_extension_name_from_path(&path_buf));
2647 let config = obj
2648 .get("config")
2649 .cloned()
2650 .unwrap_or_else(|| serde_json::json!({}));
2651 (name, path_buf, config)
2652 } else {
2653 return None;
2654 };
2655
2656 Some(crate::foreign_lsp::ConfiguredExtensionSpec { name, path, config })
2657}
2658
2659fn resolve_configured_extension_path(
2660 path: &str,
2661 workspace_root: Option<&std::path::Path>,
2662) -> std::path::PathBuf {
2663 let path = std::path::PathBuf::from(path);
2664 if path.is_absolute() {
2665 return path;
2666 }
2667 workspace_root.map(|root| root.join(&path)).unwrap_or(path)
2668}
2669
2670fn configured_extension_name_from_path(path: &std::path::Path) -> String {
2671 path.file_stem()
2672 .and_then(|stem| stem.to_str())
2673 .map(String::from)
2674 .unwrap_or_else(|| "configured-extension".to_string())
2675}
2676
2677#[cfg(test)]
2678mod tests {
2679 use super::*;
2680 use crate::util::parser_source;
2681 use tower_lsp_server::LspService;
2682
2683 #[tokio::test]
2684 async fn test_server_creation() {
2685 let (service, _socket) = LspService::new(|client| ShapeLanguageServer::new(client));
2686
2687 drop(service);
2689 }
2690
2691 #[test]
2692 fn test_frontmatter_extension_path_ranges_points_to_path_line() {
2693 let source = r#"---
2694[[extensions]]
2695name = "duckdb"
2696path = "./extensions/libshape_ext_duckdb.so"
2697---
2698let x = 1
2699"#;
2700
2701 let ranges = frontmatter_extension_path_ranges(source);
2702 assert_eq!(ranges.len(), 1);
2703 assert_eq!(ranges[0].start.line, 3);
2704 assert_eq!(ranges[0].start.character, 0);
2705 }
2706
2707 #[test]
2708 fn test_frontmatter_extension_path_ranges_handles_shebang() {
2709 let source = r#"#!/usr/bin/env shape
2710---
2711[[extensions]]
2712name = "duckdb"
2713path = "./extensions/libshape_ext_duckdb.so"
2714---
2715let x = 1
2716"#;
2717
2718 let ranges = frontmatter_extension_path_ranges(source);
2719 assert_eq!(ranges.len(), 1);
2720 assert_eq!(ranges[0].start.line, 4);
2721 }
2722
2723 #[test]
2724 fn test_validate_imports_accepts_namespace_import_from_frontmatter_extension() {
2725 let source = r#"---
2726# shape.toml
2727[[extensions]]
2728name = "duckdb"
2729path = "./extensions/libshape_ext_duckdb.so"
2730---
2731use duckdb
2732let conn = duckdb.connect("duckdb://analytics.db")
2733"#;
2734
2735 let tmp = tempfile::tempdir().unwrap();
2736 let file_path = tmp.path().join("script.shape");
2737 std::fs::write(&file_path, source).unwrap();
2738
2739 let parse_src = parser_source(source);
2740 let program = parse_program(parse_src.as_ref()).expect("program should parse");
2741 let module_cache = crate::module_cache::ModuleCache::new();
2742 let mut compiler = shape_vm::BytecodeCompiler::new();
2743
2744 let diagnostics = crate::analysis::validate_imports_and_register_items(
2745 &program,
2746 source,
2747 &file_path,
2748 &module_cache,
2749 None,
2750 &mut compiler,
2751 );
2752
2753 assert!(
2754 diagnostics.iter().all(|diag| {
2755 !diag.message.contains("Cannot resolve module ''")
2756 && !diag.message.contains("Cannot resolve module 'duckdb'")
2757 }),
2758 "namespace import from frontmatter extension should not emit resolution errors: {:?}",
2759 diagnostics
2760 );
2761 }
2762
2763 #[test]
2764 fn test_validate_imports_reports_unknown_namespace_module_name() {
2765 let source = "use missingmod\nlet x = 1\n";
2766 let tmp = tempfile::tempdir().unwrap();
2767 let file_path = tmp.path().join("script.shape");
2768 std::fs::write(&file_path, source).unwrap();
2769
2770 let parse_src = parser_source(source);
2771 let program = parse_program(parse_src.as_ref()).expect("program should parse");
2772 let module_cache = crate::module_cache::ModuleCache::new();
2773 let mut compiler = shape_vm::BytecodeCompiler::new();
2774
2775 let diagnostics = crate::analysis::validate_imports_and_register_items(
2776 &program,
2777 source,
2778 &file_path,
2779 &module_cache,
2780 None,
2781 &mut compiler,
2782 );
2783
2784 assert!(
2785 diagnostics
2786 .iter()
2787 .any(|diag| diag.message.contains("Cannot resolve module 'missingmod'")),
2788 "expected unknown namespace module diagnostic, got {:?}",
2789 diagnostics
2790 );
2791 }
2792
2793 #[test]
2794 fn test_is_position_in_frontmatter() {
2795 let source = r#"---
2796name = "script"
2797[[extensions]]
2798name = "duckdb"
2799path = "./extensions/libshape_ext_duckdb.so"
2800---
2801let x = 1
2802"#;
2803
2804 assert!(is_position_in_frontmatter(
2805 source,
2806 Position {
2807 line: 1,
2808 character: 0
2809 }
2810 ));
2811 assert!(is_position_in_frontmatter(
2812 source,
2813 Position {
2814 line: 3,
2815 character: 2
2816 }
2817 ));
2818 assert!(!is_position_in_frontmatter(
2819 source,
2820 Position {
2821 line: 6,
2822 character: 0
2823 }
2824 ));
2825 }
2826
2827 #[test]
2828 fn test_is_position_in_frontmatter_ignores_shebang_line() {
2829 let source = r#"#!/usr/bin/env shape
2830---
2831name = "script"
2832---
2833print("hello")
2834"#;
2835
2836 assert!(!is_position_in_frontmatter(
2837 source,
2838 Position {
2839 line: 0,
2840 character: 5
2841 }
2842 ));
2843 assert!(is_position_in_frontmatter(
2844 source,
2845 Position {
2846 line: 2,
2847 character: 1
2848 }
2849 ));
2850 assert!(!is_position_in_frontmatter(
2851 source,
2852 Position {
2853 line: 4,
2854 character: 0
2855 }
2856 ));
2857 }
2858
2859 #[test]
2860 fn test_configured_extensions_from_lsp_value_parses_top_level_array() {
2861 let value = serde_json::json!({
2862 "alwaysLoadExtensions": [
2863 "./extensions/libshape_ext_python.so",
2864 {
2865 "name": "duckdb",
2866 "path": "/tmp/libshape_ext_duckdb.so",
2867 "config": { "mode": "readonly" }
2868 }
2869 ]
2870 });
2871 let workspace_root = std::path::Path::new("/workspace");
2872
2873 let specs = collect_configured_extensions_from_options(Some(&value), Some(workspace_root));
2874 assert_eq!(specs.len(), 2);
2875 assert_eq!(
2876 specs[0].path,
2877 std::path::PathBuf::from("/workspace").join("./extensions/libshape_ext_python.so")
2878 );
2879 assert_eq!(specs[0].config, serde_json::json!({}));
2880 assert_eq!(specs[1].name, "duckdb");
2881 assert_eq!(
2882 specs[1].path,
2883 std::path::PathBuf::from("/tmp/libshape_ext_duckdb.so")
2884 );
2885 assert_eq!(specs[1].config, serde_json::json!({ "mode": "readonly" }));
2886 }
2887
2888 #[test]
2889 fn test_configured_extensions_from_lsp_value_parses_nested_shape_key_and_dedupes() {
2890 let value = serde_json::json!({
2891 "shape": {
2892 "always_load_extensions": [
2893 "/tmp/libshape_ext_python.so",
2894 "/tmp/libshape_ext_python.so"
2895 ]
2896 }
2897 });
2898
2899 let specs = dedup_extension_specs(collect_configured_extensions_from_options(
2900 Some(&value),
2901 None,
2902 ));
2903 assert_eq!(specs.len(), 1);
2904 assert_eq!(
2905 specs[0].path,
2906 std::path::PathBuf::from("/tmp/libshape_ext_python.so")
2907 );
2908 assert_eq!(specs[0].name, "libshape_ext_python");
2909 }
2910
2911 fn mk_token(
2914 delta_line: u32,
2915 delta_start: u32,
2916 length: u32,
2917 token_type: u32,
2918 ) -> SemanticToken {
2919 SemanticToken {
2920 delta_line,
2921 delta_start,
2922 length,
2923 token_type,
2924 token_modifiers_bitset: 0,
2925 }
2926 }
2927
2928 #[test]
2929 fn position_in_range_inclusive_start_exclusive_end() {
2930 let range = Range {
2931 start: Position { line: 1, character: 0 },
2932 end: Position { line: 3, character: 0 },
2933 };
2934 assert!(position_in_range(1, 0, &range));
2936 assert!(position_in_range(2, 5, &range));
2937 assert!(!position_in_range(3, 0, &range));
2939 assert!(!position_in_range(0, 99, &range));
2941 assert!(!position_in_range(3, 1, &range));
2943 }
2944
2945 #[test]
2946 fn position_in_range_single_line() {
2947 let range = Range {
2948 start: Position { line: 5, character: 2 },
2949 end: Position { line: 5, character: 8 },
2950 };
2951 assert!(position_in_range(5, 2, &range));
2952 assert!(position_in_range(5, 7, &range));
2953 assert!(!position_in_range(5, 8, &range));
2954 assert!(!position_in_range(5, 1, &range));
2955 assert!(!position_in_range(4, 5, &range));
2956 assert!(!position_in_range(6, 0, &range));
2957 }
2958
2959 #[test]
2960 fn compute_semantic_token_edits_empty_when_identical() {
2961 let tokens = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
2962 let edits = compute_semantic_token_edits(&tokens, &tokens);
2963 assert!(edits.is_empty(), "identical sequences must produce zero edits");
2964 }
2965
2966 #[test]
2967 fn compute_semantic_token_edits_full_replacement_when_no_overlap() {
2968 let old = vec![mk_token(0, 0, 3, 8)];
2969 let new = vec![mk_token(0, 0, 5, 5), mk_token(1, 2, 4, 8)];
2970 let edits = compute_semantic_token_edits(&old, &new);
2971 assert_eq!(edits.len(), 1);
2972 let edit = &edits[0];
2973 assert_eq!(edit.start, 0);
2975 assert_eq!(edit.delete_count, 5);
2976 assert_eq!(
2977 edit.data.as_ref().map(|d| d.len()),
2978 Some(2),
2979 "new tokens carried verbatim"
2980 );
2981 }
2982
2983 #[test]
2984 fn compute_semantic_token_edits_preserves_common_prefix() {
2985 let old = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
2986 let new = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 7, 1)];
2987 let edits = compute_semantic_token_edits(&old, &new);
2988 assert_eq!(edits.len(), 1);
2989 let edit = &edits[0];
2990 assert_eq!(edit.start, 5);
2993 assert_eq!(edit.delete_count, 5);
2994 let data = edit.data.as_ref().expect("replacement data present");
2995 assert_eq!(data.len(), 1);
2996 assert_eq!(data[0].length, 7);
2997 assert_eq!(data[0].token_type, 1);
2998 }
2999
3000 #[test]
3001 fn compute_semantic_token_edits_preserves_common_suffix() {
3002 let old = vec![mk_token(0, 0, 3, 8), mk_token(0, 4, 5, 5)];
3003 let new = vec![mk_token(0, 0, 4, 1), mk_token(0, 4, 5, 5)];
3004 let edits = compute_semantic_token_edits(&old, &new);
3005 assert_eq!(edits.len(), 1);
3006 let edit = &edits[0];
3007 assert_eq!(edit.start, 0);
3009 assert_eq!(edit.delete_count, 5);
3010 let data = edit.data.as_ref().expect("replacement data present");
3011 assert_eq!(data.len(), 1);
3012 assert_eq!(data[0].length, 4);
3013 }
3014
3015 #[test]
3016 fn compute_semantic_token_edits_pure_insertion_emits_zero_delete() {
3017 let old = vec![mk_token(0, 0, 3, 8)];
3018 let new = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
3019 let edits = compute_semantic_token_edits(&old, &new);
3020 assert_eq!(edits.len(), 1);
3021 let edit = &edits[0];
3022 assert_eq!(edit.start, 5);
3024 assert_eq!(edit.delete_count, 0);
3025 let data = edit.data.as_ref().expect("inserted tokens present");
3026 assert_eq!(data.len(), 1);
3027 assert_eq!(data[0].length, 4);
3028 }
3029
3030 #[test]
3031 fn compute_semantic_token_edits_pure_deletion_emits_none_data() {
3032 let old = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
3033 let new = vec![mk_token(0, 0, 3, 8)];
3034 let edits = compute_semantic_token_edits(&old, &new);
3035 assert_eq!(edits.len(), 1);
3036 let edit = &edits[0];
3037 assert_eq!(edit.start, 5);
3038 assert_eq!(edit.delete_count, 5);
3039 assert!(
3041 edit.data.is_none(),
3042 "pure deletion serializes as `data: null` (None)"
3043 );
3044 }
3045
3046 #[test]
3047 fn compute_semantic_token_edits_empty_to_empty_is_noop() {
3048 let edits = compute_semantic_token_edits(&[], &[]);
3049 assert!(edits.is_empty());
3050 }
3051
3052 #[test]
3053 fn compute_semantic_token_edits_empty_to_nonempty_is_insert() {
3054 let new = vec![mk_token(0, 0, 3, 8)];
3055 let edits = compute_semantic_token_edits(&[], &new);
3056 assert_eq!(edits.len(), 1);
3057 let edit = &edits[0];
3058 assert_eq!(edit.start, 0);
3059 assert_eq!(edit.delete_count, 0);
3060 assert_eq!(edit.data.as_ref().map(|d| d.len()), Some(1));
3061 }
3062
3063 #[test]
3064 fn compute_semantic_token_edits_nonempty_to_empty_is_delete() {
3065 let old = vec![mk_token(0, 0, 3, 8), mk_token(1, 0, 4, 5)];
3066 let edits = compute_semantic_token_edits(&old, &[]);
3067 assert_eq!(edits.len(), 1);
3068 let edit = &edits[0];
3069 assert_eq!(edit.start, 0);
3070 assert_eq!(edit.delete_count, 10);
3071 assert!(edit.data.is_none());
3072 }
3073
3074 #[test]
3075 fn semantic_tokens_eq_compares_all_fields() {
3076 let a = mk_token(0, 0, 3, 8);
3077 let b = mk_token(0, 0, 3, 8);
3078 assert!(semantic_tokens_eq(&a, &b));
3079
3080 let mut c = a;
3081 c.token_modifiers_bitset = 4;
3082 assert!(!semantic_tokens_eq(&a, &c));
3083
3084 let mut d = a;
3085 d.length = 99;
3086 assert!(!semantic_tokens_eq(&a, &d));
3087 }
3088
3089 #[test]
3095 fn registered_commands_cover_every_code_lens_command_w27() {
3096 let declared: std::collections::HashSet<String> =
3097 registered_commands().into_iter().collect();
3098 for cmd in &[
3100 "shape.findReferences",
3101 "shape.findImplementations",
3102 "shape.runTests",
3103 "shape.debugTests",
3104 "shape.showAnnotation",
3105 "shape.showTraitMethod",
3106 ] {
3107 assert!(
3108 declared.contains(*cmd),
3109 "executeCommand provider missing code-lens command {:?}",
3110 cmd
3111 );
3112 }
3113 }
3114
3115 #[test]
3118 fn collect_import_rewrites_rewrites_matching_from_clause_w27() {
3119 let text = "from foo::bar use { x, y };\nlet a = 1\n";
3120 let renames = vec![("foo::bar".to_string(), "baz::qux".to_string())];
3121 let edits = collect_import_rewrites(text, &renames);
3122 assert_eq!(edits.len(), 1, "expected one rewrite, got {:?}", edits);
3123 let edit = &edits[0];
3124 assert_eq!(edit.new_text, "baz::qux");
3125 assert_eq!(edit.range.start.line, 0);
3126 assert_eq!(edit.range.start.character, 5);
3128 assert_eq!(edit.range.end.character, 5 + "foo::bar".len() as u32);
3129 }
3130
3131 #[test]
3132 fn collect_import_rewrites_skips_non_matching_paths_w27() {
3133 let text = "from other::mod use { x };\nfrom foo::bar use { y };\n";
3134 let renames = vec![("foo::bar".to_string(), "new::path".to_string())];
3135 let edits = collect_import_rewrites(text, &renames);
3136 assert_eq!(edits.len(), 1, "only the foo::bar line should rewrite");
3137 assert_eq!(edits[0].range.start.line, 1);
3138 }
3139
3140 #[test]
3141 fn collect_import_rewrites_does_not_match_substring_inside_identifier_w27() {
3142 let text = "from foobar use { x };\nfrom foo use { y };\n";
3146 let renames = vec![("foo".to_string(), "renamed".to_string())];
3147 let edits = collect_import_rewrites(text, &renames);
3148 assert_eq!(edits.len(), 1, "only the bare `from foo` line should match");
3149 assert_eq!(edits[0].range.start.line, 1);
3150 assert_eq!(edits[0].new_text, "renamed");
3151 }
3152
3153 #[test]
3154 fn collect_import_rewrites_handles_indented_imports_w27() {
3155 let text = " from a::b use { x };\nfrom c::d use { y };\n";
3156 let renames = vec![
3157 ("a::b".to_string(), "AA::BB".to_string()),
3158 ("c::d".to_string(), "CC::DD".to_string()),
3159 ];
3160 let edits = collect_import_rewrites(text, &renames);
3161 assert_eq!(edits.len(), 2);
3162 assert_eq!(edits[0].range.start.character, 4 + 5);
3164 assert_eq!(edits[0].new_text, "AA::BB");
3165 assert_eq!(edits[1].range.start.line, 1);
3166 assert_eq!(edits[1].new_text, "CC::DD");
3167 }
3168
3169 #[test]
3170 fn collect_import_rewrites_returns_empty_when_no_imports_match_w27() {
3171 let text = "let x = 1\nlet y = 2\n";
3172 let renames = vec![("foo".to_string(), "bar".to_string())];
3173 assert!(collect_import_rewrites(text, &renames).is_empty());
3174 }
3175
3176 #[tokio::test]
3184 async fn workspace_symbol_eager_indexes_closed_files_w27() {
3185 use crate::document_symbols::get_workspace_symbols;
3186 let tmp = tempfile::tempdir().unwrap();
3187 let root = tmp.path();
3188 let open_path = root.join("open.shape");
3192 let closed_path = root.join("nested/closed.shape");
3193 std::fs::create_dir_all(closed_path.parent().unwrap()).unwrap();
3194 std::fs::write(&open_path, "fn open_sym() { 1 }\n").unwrap();
3195 std::fs::write(&closed_path, "fn closed_sym() { 2 }\n").unwrap();
3196
3197 let cache = crate::module_cache::ModuleCache::new();
3198 let files = cache.enumerate_workspace_shape_files(root);
3199 let mut found_closed = false;
3200 for path in files {
3201 if path != closed_path {
3202 continue;
3203 }
3204 let uri = Uri::from_file_path(&path).unwrap();
3205 let text = std::fs::read_to_string(&path).unwrap();
3206 let symbols = get_workspace_symbols(&text, &uri, "closed_sym");
3207 assert!(
3208 symbols.iter().any(|s| s.name == "closed_sym"),
3209 "closed-file symbol must appear in workspace results: {:?}",
3210 symbols
3211 );
3212 found_closed = true;
3213 }
3214 assert!(
3215 found_closed,
3216 "workspace enumerator must surface the closed.shape file"
3217 );
3218 }
3219
3220 #[test]
3227 fn did_change_watched_files_invalidates_module_cache_w27() {
3228 let tmp = tempfile::tempdir().unwrap();
3229 let path = tmp.path().join("watched.shape");
3230 std::fs::write(&path, "pub fn before() { 1 }\n").unwrap();
3231 let cache = crate::module_cache::ModuleCache::new();
3232 let info1 = cache
3233 .load_module_with_context(&path, &path, None)
3234 .expect("first load must succeed");
3235 assert!(
3236 info1.exports.iter().any(|e| e.name == "before"),
3237 "before-invalidate export should be visible: {:?}",
3238 info1.exports
3239 );
3240
3241 std::fs::write(&path, "pub fn after() { 2 }\n").unwrap();
3244 cache.invalidate(&path);
3245
3246 let info2 = cache
3247 .load_module_with_context(&path, &path, None)
3248 .expect("second load must succeed");
3249 assert!(
3250 info2.exports.iter().any(|e| e.name == "after"),
3251 "after-invalidate export should reflect the new file contents: {:?}",
3252 info2.exports
3253 );
3254 assert!(
3255 !info2.exports.iter().any(|e| e.name == "before"),
3256 "stale pre-invalidate export must NOT linger: {:?}",
3257 info2.exports
3258 );
3259 }
3260}