document.rs
1 use crate::analysis::resolver::BindingResolver; 2 use crate::analysis::{AnalysisPipeline, BindingGraph, QueryEngine}; 3 use crate::languages::{LanguageRegistry, LanguageSupport}; 4 use crate::types::{ 5 BindingKind, DocumentState, EnvBinding, EnvBindingUsage, EnvReference, ExportResolution, 6 FileExportEntry, ImportContext, SymbolId, SymbolOrigin, 7 }; 8 use compact_str::CompactString; 9 use dashmap::DashMap; 10 use std::sync::Arc; 11 use tower_lsp::lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url}; 12 use tree_sitter::{InputEdit, Point, Tree}; 13 14 /// Information about an edit for incremental analysis. 15 #[derive(Debug, Clone)] 16 pub struct EditInfo { 17 /// The range that was edited (None for full document replacement) 18 pub range: Option<Range>, 19 /// Whether this is a full document replacement 20 pub is_full_replacement: bool, 21 } 22 23 impl EditInfo { 24 pub fn full_replacement() -> Self { 25 Self { 26 range: None, 27 is_full_replacement: true, 28 } 29 } 30 31 pub fn incremental(range: Range) -> Self { 32 Self { 33 range: Some(range), 34 is_full_replacement: false, 35 } 36 } 37 } 38 39 #[derive(Debug, Clone, Default)] 40 pub struct DocumentAnalysis { 41 pub import_context: ImportContext, 42 pub binding_graph: Arc<BindingGraph>, 43 pub exports: FileExportEntry, 44 pub syntax_errors: Vec<(Range, Option<String>)>, 45 } 46 47 struct AnalysisResult { 48 tree: Option<Tree>, 49 analysis: DocumentAnalysis, 50 } 51 52 pub struct DocumentEntry { 53 pub state: DocumentState, 54 pub analysis: Arc<DocumentAnalysis>, 55 } 56 57 pub struct DocumentManager { 58 documents: DashMap<Url, DocumentEntry>, 59 query_engine: Arc<QueryEngine>, 60 languages: Arc<LanguageRegistry>, 61 } 62 63 impl DocumentManager { 64 pub fn new(query_engine: Arc<QueryEngine>, languages: Arc<LanguageRegistry>) -> Self { 65 Self { 66 documents: DashMap::new(), 67 query_engine, 68 languages, 69 } 70 } 71 72 pub async fn open(&self, uri: Url, language_id: String, content: String, version: i32) { 73 let lang_opt = self 74 .languages 75 .get_by_language_id(&language_id) 76 .or_else(|| self.languages.get_for_uri(&uri)); 77 78 let mut doc = DocumentState::new( 79 uri.clone(), 80 CompactString::from(&language_id), 81 content.clone(), 82 version, 83 ); 84 85 let analysis = if let Some(lang) = lang_opt { 86 let AnalysisResult { tree, analysis } = 87 self.analyze_content(&content, lang.as_ref(), None).await; 88 89 doc.tree = tree; 90 doc.import_context = analysis.import_context.clone(); 91 Arc::new(analysis) 92 } else { 93 Arc::new(DocumentAnalysis::default()) 94 }; 95 96 self.documents.insert( 97 uri.clone(), 98 DocumentEntry { 99 state: doc, 100 analysis, 101 }, 102 ); 103 } 104 105 pub async fn change( 106 &self, 107 uri: &Url, 108 changes: Vec<TextDocumentContentChangeEvent>, 109 version: i32, 110 ) { 111 let (content, language_id, old_tree) = { 112 let Some(mut entry) = self.documents.get_mut(uri) else { 113 return; 114 }; 115 116 let mut content = entry.state.content.as_ref().clone(); 117 let mut tree = entry.state.tree.clone(); 118 119 for change in &changes { 120 let Some(range) = change.range else { 121 tracing::error!( 122 uri = %uri, 123 "Rejected non-incremental change payload: missing range" 124 ); 125 return; 126 }; 127 128 let Some((start_byte, old_end_byte)) = range_to_byte_offsets(&content, range) 129 else { 130 tracing::warn!( 131 uri = %uri, 132 "Failed to map change range to byte offsets; dropping change" 133 ); 134 return; 135 }; 136 137 let start_position = byte_offset_to_point(&content, start_byte); 138 let old_end_position = byte_offset_to_point(&content, old_end_byte); 139 140 content.replace_range(start_byte..old_end_byte, &change.text); 141 142 let new_end_byte = start_byte + change.text.len(); 143 let new_end_position = byte_offset_to_point(&content, new_end_byte); 144 145 if let Some(current_tree) = tree.as_mut() { 146 current_tree.edit(&InputEdit { 147 start_byte, 148 old_end_byte, 149 new_end_byte, 150 start_position, 151 old_end_position, 152 new_end_position, 153 }); 154 } 155 } 156 157 entry.state.content = Arc::new(content.clone()); 158 entry.state.version = version; 159 160 (content, entry.state.language_id.clone(), tree) 161 }; 162 163 let lang_opt = self 164 .languages 165 .get_by_language_id(&language_id) 166 .or_else(|| self.languages.get_for_uri(uri)); 167 168 if let Some(lang) = lang_opt { 169 let AnalysisResult { tree, analysis } = self 170 .analyze_content_with_edit(&content, lang.as_ref(), old_tree.as_ref()) 171 .await; 172 173 if let Some(mut entry) = self.documents.get_mut(uri) { 174 if entry.state.version == version { 175 entry.state.tree = tree; 176 entry.state.import_context = analysis.import_context.clone(); 177 entry.analysis = Arc::new(analysis); 178 } 179 } 180 } 181 } 182 183 pub fn close(&self, uri: &Url) { 184 self.documents.remove(uri); 185 } 186 187 async fn analyze_content( 188 &self, 189 content: &str, 190 language: &dyn LanguageSupport, 191 old_tree: Option<&Tree>, 192 ) -> AnalysisResult { 193 // Pass old_tree for incremental parsing when available 194 let tree = self.query_engine.parse(language, content, old_tree).await; 195 196 let Some(tree) = &tree else { 197 return AnalysisResult { 198 tree: None, 199 analysis: DocumentAnalysis::default(), 200 }; 201 }; 202 203 let source = content.as_bytes(); 204 205 let imports = self 206 .query_engine 207 .extract_imports(language, tree, source) 208 .await; 209 210 let mut import_ctx = ImportContext::new(); 211 for import in &imports { 212 import_ctx 213 .imported_modules 214 .insert(import.module_path.clone()); 215 216 if let Some(alias) = &import.alias { 217 import_ctx.aliases.insert( 218 alias.clone(), 219 (import.module_path.clone(), import.original_name.clone()), 220 ); 221 } else { 222 import_ctx.aliases.insert( 223 import.original_name.clone(), 224 (import.module_path.clone(), import.original_name.clone()), 225 ); 226 } 227 } 228 229 let binding_graph = 230 AnalysisPipeline::analyze(&self.query_engine, language, tree, source, &import_ctx) 231 .await; 232 233 let mut exports = self 234 .query_engine 235 .extract_exports(language, tree, source) 236 .await; 237 resolve_export_resolutions(&mut exports, &binding_graph); 238 239 let mut syntax_errors = Vec::new(); 240 collect_error_nodes(tree.root_node(), source, &mut syntax_errors); 241 242 AnalysisResult { 243 tree: Some(tree.clone()), 244 analysis: DocumentAnalysis { 245 import_context: import_ctx, 246 binding_graph: Arc::new(binding_graph), 247 exports, 248 syntax_errors, 249 }, 250 } 251 } 252 253 async fn analyze_content_with_edit( 254 &self, 255 content: &str, 256 language: &dyn LanguageSupport, 257 old_tree: Option<&Tree>, 258 ) -> AnalysisResult { 259 self.analyze_content(content, language, old_tree).await 260 } 261 262 pub fn get( 263 &self, 264 uri: &Url, 265 ) -> Option<dashmap::mapref::one::MappedRef<'_, Url, DocumentEntry, DocumentState>> { 266 self.documents.get(uri).map(|entry| entry.map(|e| &e.state)) 267 } 268 269 pub fn get_env_reference_cloned(&self, uri: &Url, position: Position) -> Option<EnvReference> { 270 let entry = self.documents.get(uri)?; 271 let resolver = BindingResolver::new(&entry.analysis.binding_graph); 272 resolver.get_env_reference_cloned(position) 273 } 274 275 pub fn get_env_binding_cloned(&self, uri: &Url, position: Position) -> Option<EnvBinding> { 276 let entry = self.documents.get(uri)?; 277 let resolver = BindingResolver::new(&entry.analysis.binding_graph); 278 resolver.get_env_binding_cloned(position) 279 } 280 281 pub fn get_binding_usage_cloned( 282 &self, 283 uri: &Url, 284 position: Position, 285 ) -> Option<EnvBindingUsage> { 286 let entry = self.documents.get(uri)?; 287 let resolver = BindingResolver::new(&entry.analysis.binding_graph); 288 resolver.get_binding_usage_cloned(position) 289 } 290 291 pub fn get_binding_kind_for_usage(&self, uri: &Url, binding_name: &str) -> Option<BindingKind> { 292 let entry = self.documents.get(uri)?; 293 let resolver = BindingResolver::new(&entry.analysis.binding_graph); 294 resolver.get_binding_kind(binding_name) 295 } 296 297 pub async fn check_completion(&self, uri: &Url, position: Position) -> bool { 298 let (tree, content, language_id, analysis) = { 299 let entry = match self.documents.get(uri) { 300 Some(e) => e, 301 None => return false, 302 }; 303 let tree = match &entry.state.tree { 304 Some(t) => t.clone(), 305 None => return false, 306 }; 307 let content = entry.state.content.clone(); 308 let language_id = entry.state.language_id.clone(); 309 let analysis = Arc::clone(&entry.analysis); 310 (tree, content, language_id, analysis) 311 }; 312 313 let lang = match self.languages.get_by_language_id(&language_id) { 314 Some(l) => l, 315 None => return false, 316 }; 317 318 let obj_name_opt = self 319 .query_engine 320 .check_completion_context(lang.as_ref(), &tree, content.as_bytes(), position) 321 .await; 322 323 if let Some(obj_name) = obj_name_opt { 324 if lang.is_standard_env_object(&obj_name) { 325 return true; 326 } 327 328 let resolver = BindingResolver::new(&analysis.binding_graph); 329 if let Some(kind) = resolver.get_binding_kind(&obj_name) { 330 if kind == BindingKind::Object { 331 return true; 332 } 333 } 334 } 335 false 336 } 337 338 pub async fn check_completion_context( 339 &self, 340 uri: &Url, 341 position: Position, 342 ) -> Option<CompactString> { 343 let (tree, content, language_id) = { 344 let entry = match self.documents.get(uri) { 345 Some(e) => e, 346 None => return None, 347 }; 348 let tree = match entry.state.tree.clone() { 349 Some(t) => t, 350 None => return None, 351 }; 352 let content = entry.state.content.clone(); 353 let language_id = entry.state.language_id.clone(); 354 (tree, content, language_id) 355 }; 356 357 let lang = self.languages.get_by_language_id(&language_id)?; 358 359 self.query_engine 360 .check_completion_context(lang.as_ref(), &tree, content.as_bytes(), position) 361 .await 362 } 363 364 pub fn get_binding_graph(&self, uri: &Url) -> Option<Arc<BindingGraph>> { 365 self.documents 366 .get(uri) 367 .map(|entry| Arc::clone(&entry.analysis.binding_graph)) 368 } 369 370 pub fn get_analysis(&self, uri: &Url) -> Option<Arc<DocumentAnalysis>> { 371 self.documents 372 .get(uri) 373 .map(|entry| Arc::clone(&entry.analysis)) 374 } 375 376 pub fn get_import_context(&self, uri: &Url) -> Option<ImportContext> { 377 self.documents 378 .get(uri) 379 .map(|entry| entry.analysis.import_context.clone()) 380 } 381 382 pub fn all_uris(&self) -> Vec<Url> { 383 self.documents 384 .iter() 385 .map(|entry| entry.key().clone()) 386 .collect() 387 } 388 389 /// Returns the number of open documents. 390 pub fn document_count(&self) -> usize { 391 self.documents.len() 392 } 393 394 pub fn query_engine(&self) -> &Arc<QueryEngine> { 395 &self.query_engine 396 } 397 398 /// Check if the document has any syntax errors. 399 pub fn has_syntax_errors(&self, uri: &Url) -> Option<bool> { 400 let entry = self.documents.get(uri)?; 401 Some(!entry.analysis.syntax_errors.is_empty()) 402 } 403 404 /// Get syntax error locations and messages from the parsed tree. 405 /// Returns a list of (Range, Option<message>) for each error node. 406 pub fn get_syntax_errors(&self, uri: &Url) -> Vec<(Range, Option<String>)> { 407 let entry = match self.documents.get(uri) { 408 Some(e) => e, 409 None => return Vec::new(), 410 }; 411 entry.analysis.syntax_errors.clone() 412 } 413 } 414 415 /// Recursively collect ERROR and MISSING nodes from the tree. 416 fn collect_error_nodes( 417 node: tree_sitter::Node, 418 source: &[u8], 419 errors: &mut Vec<(Range, Option<String>)>, 420 ) { 421 if node.is_error() { 422 let range = node_to_lsp_range(node); 423 let text = node 424 .utf8_text(source) 425 .ok() 426 .map(|s| format!("Unexpected: {}", truncate_text(s, 30))); 427 errors.push((range, text)); 428 } else if node.is_missing() { 429 let range = node_to_lsp_range(node); 430 let message = Some(format!("Missing: {}", node.kind())); 431 errors.push((range, message)); 432 } 433 434 // Recurse into children 435 let mut cursor = node.walk(); 436 for child in node.children(&mut cursor) { 437 collect_error_nodes(child, source, errors); 438 } 439 } 440 441 /// Convert tree-sitter node to LSP range. 442 fn node_to_lsp_range(node: tree_sitter::Node) -> Range { 443 let start = node.start_position(); 444 let end = node.end_position(); 445 Range { 446 start: Position { 447 line: start.row as u32, 448 character: start.column as u32, 449 }, 450 end: Position { 451 line: end.row as u32, 452 character: end.column as u32, 453 }, 454 } 455 } 456 457 /// Truncate text with ellipsis if too long. 458 fn truncate_text(text: &str, max_len: usize) -> String { 459 let trimmed = text.trim(); 460 if trimmed.len() > max_len { 461 format!("{}...", &trimmed[..max_len]) 462 } else { 463 trimmed.to_string() 464 } 465 } 466 467 fn range_to_byte_offsets(content: &str, range: Range) -> Option<(usize, usize)> { 468 let start_char = lsp_position_to_char_offset(content, range.start)?; 469 let end_char = lsp_position_to_char_offset(content, range.end)?; 470 if end_char < start_char { 471 return None; 472 } 473 474 let rope = ropey::Rope::from_str(content); 475 let start_byte = rope.try_char_to_byte(start_char).ok()?; 476 let end_byte = rope.try_char_to_byte(end_char).ok()?; 477 Some((start_byte, end_byte)) 478 } 479 480 fn lsp_position_to_char_offset(content: &str, position: Position) -> Option<usize> { 481 let rope = ropey::Rope::from_str(content); 482 let line_idx = position.line as usize; 483 let line = rope.get_line(line_idx)?; 484 let line_chars = line.len_chars(); 485 let line_text = line.to_string(); 486 let line_char_col = utf16_column_to_char_offset(&line_text, position.character as usize)?; 487 if line_char_col > line_chars { 488 return None; 489 } 490 let line_start = rope.try_line_to_char(line_idx).ok()?; 491 Some(line_start + line_char_col) 492 } 493 494 fn utf16_column_to_char_offset(text: &str, utf16_col: usize) -> Option<usize> { 495 let mut units = 0usize; 496 for (char_offset, ch) in text.chars().enumerate() { 497 if units == utf16_col { 498 return Some(char_offset); 499 } 500 units += ch.len_utf16(); 501 if units > utf16_col { 502 return None; 503 } 504 } 505 if units == utf16_col { 506 Some(text.chars().count()) 507 } else { 508 None 509 } 510 } 511 512 fn byte_offset_to_point(content: &str, byte_offset: usize) -> Point { 513 let rope = ropey::Rope::from_str(content); 514 let line_idx = rope.byte_to_line(byte_offset); 515 let line_start_byte = rope.line_to_byte(line_idx); 516 Point { 517 row: line_idx, 518 column: byte_offset.saturating_sub(line_start_byte), 519 } 520 } 521 522 fn resolve_export_resolutions(exports: &mut FileExportEntry, graph: &BindingGraph) { 523 fn resolve_symbol_chain( 524 graph: &BindingGraph, 525 symbol_id: SymbolId, 526 depth: usize, 527 ) -> Option<(Option<CompactString>, Option<CompactString>)> { 528 const MAX_DEPTH: usize = 20; 529 if depth >= MAX_DEPTH { 530 return None; 531 } 532 533 let symbol = graph.get_symbol(symbol_id)?; 534 match &symbol.origin { 535 SymbolOrigin::EnvVar { name } => Some((Some(name.clone()), None)), 536 SymbolOrigin::EnvObject { canonical_name } => { 537 Some((None, Some(canonical_name.clone()))) 538 } 539 SymbolOrigin::Symbol { target } => resolve_symbol_chain(graph, *target, depth + 1), 540 SymbolOrigin::DestructuredProperty { source, key } => { 541 if let Some((_, Some(_))) = resolve_symbol_chain(graph, *source, depth + 1) { 542 Some((Some(key.clone()), None)) 543 } else { 544 None 545 } 546 } 547 _ => None, 548 } 549 } 550 551 let resolve_symbol = |local_name: &str| -> ExportResolution { 552 let resolver = BindingResolver::new(graph); 553 554 if let Some(kind) = resolver.get_binding_kind(local_name) { 555 if kind == crate::types::BindingKind::Object { 556 for symbol in graph.symbols() { 557 if symbol.name.as_str() == local_name && symbol.is_valid { 558 if let SymbolOrigin::EnvObject { canonical_name } = &symbol.origin { 559 return ExportResolution::EnvObject { 560 canonical_name: canonical_name.clone(), 561 }; 562 } 563 } 564 } 565 return ExportResolution::EnvObject { 566 canonical_name: local_name.into(), 567 }; 568 } 569 } 570 571 for symbol in graph.symbols() { 572 if symbol.name.as_str() == local_name && symbol.is_valid { 573 match &symbol.origin { 574 SymbolOrigin::EnvVar { name } => { 575 return ExportResolution::EnvVar { name: name.clone() }; 576 } 577 SymbolOrigin::EnvObject { canonical_name } => { 578 return ExportResolution::EnvObject { 579 canonical_name: canonical_name.clone(), 580 }; 581 } 582 SymbolOrigin::Symbol { target } => { 583 if let Some((env_var, env_obj)) = resolve_symbol_chain(graph, *target, 0) { 584 if let Some(name) = env_var { 585 return ExportResolution::EnvVar { name }; 586 } 587 if let Some(canonical_name) = env_obj { 588 return ExportResolution::EnvObject { canonical_name }; 589 } 590 } 591 } 592 SymbolOrigin::DestructuredProperty { source, key } => { 593 if let Some((_, Some(_))) = resolve_symbol_chain(graph, *source, 0) { 594 return ExportResolution::EnvVar { name: key.clone() }; 595 } 596 } 597 SymbolOrigin::Unknown 598 | SymbolOrigin::UnresolvedSymbol { .. } 599 | SymbolOrigin::UnresolvedDestructure { .. } 600 | SymbolOrigin::Unresolvable => {} 601 } 602 } 603 } 604 605 ExportResolution::Unknown 606 }; 607 608 for export in exports.named_exports.values_mut() { 609 if matches!(export.resolution, ExportResolution::Unknown) { 610 let resolution = resolve_symbol(export.exported_name.as_str()); 611 export.resolution = if matches!(resolution, ExportResolution::Unknown) { 612 if let Some(ref local_name) = export.local_name { 613 resolve_symbol(local_name.as_str()) 614 } else { 615 resolution 616 } 617 } else { 618 resolution 619 }; 620 } 621 } 622 623 if let Some(ref mut default) = exports.default_export { 624 if matches!(default.resolution, ExportResolution::Unknown) { 625 if let Some(ref local_name) = default.local_name { 626 default.resolution = resolve_symbol(local_name.as_str()); 627 } else if default.exported_name != "default" { 628 default.resolution = resolve_symbol(default.exported_name.as_str()); 629 } 630 } 631 } 632 } 633 634 #[cfg(test)] 635 mod tests { 636 use super::*; 637 use crate::languages::go::Go; 638 use crate::languages::javascript::JavaScript; 639 use crate::languages::python::Python; 640 use crate::languages::rust::Rust; 641 use crate::languages::typescript::{TypeScript, TypeScriptReact}; 642 use crate::languages::LanguageRegistry; 643 644 fn create_test_manager() -> DocumentManager { 645 let query_engine = Arc::new(QueryEngine::new()); 646 let mut registry = LanguageRegistry::new(); 647 registry.register(Arc::new(JavaScript)); 648 registry.register(Arc::new(TypeScript)); 649 registry.register(Arc::new(TypeScriptReact)); 650 registry.register(Arc::new(Python)); 651 registry.register(Arc::new(Rust)); 652 registry.register(Arc::new(Go)); 653 let languages = Arc::new(registry); 654 DocumentManager::new(query_engine, languages) 655 } 656 657 fn test_uri(name: &str) -> Url { 658 Url::parse(&format!("file:///test/{}", name)).unwrap() 659 } 660 661 #[tokio::test] 662 async fn test_open_javascript_document() { 663 let manager = create_test_manager(); 664 let uri = test_uri("test.js"); 665 let content = r#"const db = process.env.DATABASE_URL;"#.to_string(); 666 667 manager 668 .open(uri.clone(), "javascript".to_string(), content, 1) 669 .await; 670 671 let doc = manager.get(&uri).unwrap(); 672 assert_eq!(doc.version, 1); 673 assert_eq!(doc.language_id, "javascript"); 674 assert!(doc.tree.is_some()); 675 } 676 677 #[tokio::test] 678 async fn test_open_typescript_document() { 679 let manager = create_test_manager(); 680 let uri = test_uri("test.ts"); 681 let content = r#"const apiKey: string = process.env.API_KEY || '';"#.to_string(); 682 683 manager 684 .open(uri.clone(), "typescript".to_string(), content, 1) 685 .await; 686 687 let doc = manager.get(&uri).unwrap(); 688 assert_eq!(doc.language_id, "typescript"); 689 assert!(doc.tree.is_some()); 690 } 691 692 #[tokio::test] 693 async fn test_open_python_document() { 694 let manager = create_test_manager(); 695 let uri = test_uri("test.py"); 696 let content = r#"import os 697 db_url = os.environ.get("DATABASE_URL")"# 698 .to_string(); 699 700 manager 701 .open(uri.clone(), "python".to_string(), content, 1) 702 .await; 703 704 let doc = manager.get(&uri).unwrap(); 705 assert_eq!(doc.language_id, "python"); 706 assert!(doc.tree.is_some()); 707 } 708 709 #[tokio::test] 710 async fn test_open_rust_document() { 711 let manager = create_test_manager(); 712 let uri = test_uri("test.rs"); 713 let content = r#"fn main() { 714 let api_key = std::env::var("API_KEY").unwrap(); 715 }"# 716 .to_string(); 717 718 manager 719 .open(uri.clone(), "rust".to_string(), content, 1) 720 .await; 721 722 let doc = manager.get(&uri).unwrap(); 723 assert_eq!(doc.language_id, "rust"); 724 assert!(doc.tree.is_some()); 725 } 726 727 #[tokio::test] 728 async fn test_open_go_document() { 729 let manager = create_test_manager(); 730 let uri = test_uri("test.go"); 731 let content = r#"package main 732 import "os" 733 func main() { 734 apiKey := os.Getenv("API_KEY") 735 }"# 736 .to_string(); 737 738 manager 739 .open(uri.clone(), "go".to_string(), content, 1) 740 .await; 741 742 let doc = manager.get(&uri).unwrap(); 743 assert_eq!(doc.language_id, "go"); 744 assert!(doc.tree.is_some()); 745 } 746 747 #[tokio::test] 748 async fn test_open_unknown_language() { 749 let manager = create_test_manager(); 750 let uri = test_uri("test.unknown"); 751 let content = "some content".to_string(); 752 753 manager 754 .open(uri.clone(), "unknown".to_string(), content, 1) 755 .await; 756 757 let doc = manager.get(&uri).unwrap(); 758 assert_eq!(doc.language_id, "unknown"); 759 760 assert!(doc.tree.is_none()); 761 } 762 763 #[tokio::test] 764 async fn test_change_document() { 765 let manager = create_test_manager(); 766 let uri = test_uri("test.js"); 767 let content = r#"const x = 1;"#.to_string(); 768 769 manager 770 .open(uri.clone(), "javascript".to_string(), content, 1) 771 .await; 772 773 let new_content = r#"const db = process.env.DATABASE_URL;"#.to_string(); 774 let changes = vec![TextDocumentContentChangeEvent { 775 range: Some(Range::new(Position::new(0, 0), Position::new(0, 12))), 776 range_length: None, 777 text: new_content.clone(), 778 }]; 779 780 manager.change(&uri, changes, 2).await; 781 782 let doc = manager.get(&uri).unwrap(); 783 assert_eq!(doc.version, 2); 784 assert_eq!(doc.content.as_str(), new_content); 785 } 786 787 #[tokio::test] 788 async fn test_change_nonexistent_document() { 789 let manager = create_test_manager(); 790 let uri = test_uri("nonexistent.js"); 791 792 let changes = vec![TextDocumentContentChangeEvent { 793 range: Some(Range::new(Position::new(0, 0), Position::new(0, 0))), 794 range_length: None, 795 text: "new content".to_string(), 796 }]; 797 798 manager.change(&uri, changes, 1).await; 799 } 800 801 #[tokio::test] 802 async fn test_get_env_reference_cloned() { 803 let manager = create_test_manager(); 804 let uri = test_uri("test.js"); 805 806 let content = r#"const x = process.env.DATABASE_URL;"#.to_string(); 807 808 manager 809 .open(uri.clone(), "javascript".to_string(), content, 1) 810 .await; 811 812 let reference = manager.get_env_reference_cloned(&uri, Position::new(0, 22)); 813 assert!(reference.is_some()); 814 let reference = reference.unwrap(); 815 assert_eq!(reference.name, "DATABASE_URL"); 816 } 817 818 #[tokio::test] 819 async fn test_get_env_binding_cloned() { 820 let manager = create_test_manager(); 821 let uri = test_uri("test.js"); 822 823 let content = r#"const { API_KEY } = process.env;"#.to_string(); 824 825 manager 826 .open(uri.clone(), "javascript".to_string(), content, 1) 827 .await; 828 829 let binding = manager.get_env_binding_cloned(&uri, Position::new(0, 10)); 830 assert!(binding.is_some()); 831 let binding = binding.unwrap(); 832 assert_eq!(binding.binding_name, "API_KEY"); 833 assert_eq!(binding.env_var_name, "API_KEY"); 834 } 835 836 #[tokio::test] 837 async fn test_get_binding_usage_cloned() { 838 let manager = create_test_manager(); 839 let uri = test_uri("test.js"); 840 841 let content = r#"const { API_KEY } = process.env; 842 console.log(API_KEY);"# 843 .to_string(); 844 845 manager 846 .open(uri.clone(), "javascript".to_string(), content, 1) 847 .await; 848 849 let usage = manager.get_binding_usage_cloned(&uri, Position::new(1, 14)); 850 851 assert!(usage.is_none() || usage.is_some()); 852 } 853 854 #[tokio::test] 855 async fn test_get_binding_kind_for_usage() { 856 let manager = create_test_manager(); 857 let uri = test_uri("test.js"); 858 let content = r#"const env = process.env; 859 const { API_KEY } = env;"# 860 .to_string(); 861 862 manager 863 .open(uri.clone(), "javascript".to_string(), content, 1) 864 .await; 865 866 let kind = manager.get_binding_kind_for_usage(&uri, "env"); 867 assert!(kind.is_some()); 868 assert_eq!(kind.unwrap(), BindingKind::Object); 869 } 870 871 #[tokio::test] 872 async fn test_check_completion() { 873 let manager = create_test_manager(); 874 let uri = test_uri("test.js"); 875 876 let content = r#"process.env."#.to_string(); 877 878 manager 879 .open(uri.clone(), "javascript".to_string(), content, 1) 880 .await; 881 882 let should_complete = manager.check_completion(&uri, Position::new(0, 12)).await; 883 884 assert!(should_complete); 885 } 886 887 #[tokio::test] 888 async fn test_check_completion_on_alias() { 889 let manager = create_test_manager(); 890 let uri = test_uri("test.js"); 891 let content = r#"const env = process.env; 892 env."# 893 .to_string(); 894 895 manager 896 .open(uri.clone(), "javascript".to_string(), content, 1) 897 .await; 898 899 let should_complete = manager.check_completion(&uri, Position::new(1, 4)).await; 900 901 assert!(should_complete); 902 } 903 904 #[tokio::test] 905 async fn test_check_completion_context() { 906 let manager = create_test_manager(); 907 let uri = test_uri("test.js"); 908 909 let content = r#"process.env."#.to_string(); 910 911 manager 912 .open(uri.clone(), "javascript".to_string(), content, 1) 913 .await; 914 915 let context = manager 916 .check_completion_context(&uri, Position::new(0, 12)) 917 .await; 918 assert!(context.is_some()); 919 assert_eq!(context.unwrap(), "process.env"); 920 } 921 922 #[tokio::test] 923 async fn test_get_binding_graph() { 924 let manager = create_test_manager(); 925 let uri = test_uri("test.js"); 926 let content = r#"const db = process.env.DATABASE_URL;"#.to_string(); 927 928 manager 929 .open(uri.clone(), "javascript".to_string(), content, 1) 930 .await; 931 932 let graph = manager.get_binding_graph(&uri); 933 assert!(graph.is_some()); 934 935 let graph = graph.unwrap(); 936 937 assert!(!graph.direct_references().is_empty()); 938 } 939 940 #[tokio::test] 941 async fn test_all_uris() { 942 let manager = create_test_manager(); 943 let uri1 = test_uri("test1.js"); 944 let uri2 = test_uri("test2.js"); 945 946 manager 947 .open( 948 uri1.clone(), 949 "javascript".to_string(), 950 "const x = 1;".to_string(), 951 1, 952 ) 953 .await; 954 manager 955 .open( 956 uri2.clone(), 957 "javascript".to_string(), 958 "const y = 2;".to_string(), 959 1, 960 ) 961 .await; 962 963 let uris = manager.all_uris(); 964 assert_eq!(uris.len(), 2); 965 assert!(uris.contains(&uri1)); 966 assert!(uris.contains(&uri2)); 967 } 968 969 #[tokio::test] 970 async fn test_get_nonexistent_document() { 971 let manager = create_test_manager(); 972 let uri = test_uri("nonexistent.js"); 973 974 let doc = manager.get(&uri); 975 assert!(doc.is_none()); 976 } 977 978 #[tokio::test] 979 async fn test_query_engine_access() { 980 let manager = create_test_manager(); 981 let _engine = manager.query_engine(); 982 } 983 984 #[tokio::test] 985 async fn test_document_with_imports() { 986 let manager = create_test_manager(); 987 let uri = test_uri("test.js"); 988 let content = r#"import { env } from 'process'; 989 const db = env.DATABASE_URL;"# 990 .to_string(); 991 992 manager 993 .open(uri.clone(), "javascript".to_string(), content, 1) 994 .await; 995 996 let doc = manager.get(&uri).unwrap(); 997 998 let _import_ctx = &doc.import_context; 999 } 1000 1001 #[tokio::test] 1002 async fn test_complex_binding_chain() { 1003 let manager = create_test_manager(); 1004 let uri = test_uri("test.js"); 1005 let content = r#"const env = process.env; 1006 const config = env; 1007 const { DATABASE_URL } = config; 1008 console.log(DATABASE_URL);"# 1009 .to_string(); 1010 1011 manager 1012 .open(uri.clone(), "javascript".to_string(), content, 1) 1013 .await; 1014 1015 let graph = manager.get_binding_graph(&uri); 1016 assert!(graph.is_some()); 1017 1018 let graph = graph.unwrap(); 1019 1020 assert!(graph.symbols().len() >= 2); 1021 } 1022 1023 #[tokio::test] 1024 async fn test_tsx_document() { 1025 let manager = create_test_manager(); 1026 let uri = test_uri("test.tsx"); 1027 let content = r#"const Component = () => { 1028 const apiKey = process.env.API_KEY; 1029 return <div>{apiKey}</div>; 1030 };"# 1031 .to_string(); 1032 1033 manager 1034 .open(uri.clone(), "typescriptreact".to_string(), content, 1) 1035 .await; 1036 1037 let doc = manager.get(&uri).unwrap(); 1038 assert_eq!(doc.language_id, "typescriptreact"); 1039 assert!(doc.tree.is_some()); 1040 } 1041 1042 #[tokio::test] 1043 async fn test_version_mismatch_on_change() { 1044 let manager = create_test_manager(); 1045 let uri = test_uri("test.js"); 1046 let content = r#"const x = 1;"#.to_string(); 1047 1048 manager 1049 .open(uri.clone(), "javascript".to_string(), content, 1) 1050 .await; 1051 1052 let changes1 = vec![TextDocumentContentChangeEvent { 1053 range: Some(Range::new(Position::new(0, 0), Position::new(0, 12))), 1054 range_length: None, 1055 text: "const x = 2;".to_string(), 1056 }]; 1057 manager.change(&uri, changes1, 2).await; 1058 1059 let changes2 = vec![TextDocumentContentChangeEvent { 1060 range: Some(Range::new(Position::new(0, 0), Position::new(0, 12))), 1061 range_length: None, 1062 text: "const x = 3;".to_string(), 1063 }]; 1064 manager.change(&uri, changes2, 3).await; 1065 1066 let doc = manager.get(&uri).unwrap(); 1067 assert_eq!(doc.version, 3); 1068 assert_eq!(doc.content.as_str(), "const x = 3;"); 1069 } 1070 1071 #[tokio::test] 1072 async fn test_uri_by_extension_detection() { 1073 let manager = create_test_manager(); 1074 1075 let uri = Url::parse("file:///test/test.js").unwrap(); 1076 let content = r#"const db = process.env.DATABASE_URL;"#.to_string(); 1077 1078 manager.open(uri.clone(), "".to_string(), content, 1).await; 1079 1080 let doc = manager.get(&uri).unwrap(); 1081 1082 assert!(doc.tree.is_some()); 1083 } 1084 1085 #[tokio::test] 1086 async fn test_multiple_env_references() { 1087 let manager = create_test_manager(); 1088 let uri = test_uri("test.js"); 1089 let content = r#"const db = process.env.DATABASE_URL; 1090 const api = process.env.API_KEY; 1091 const secret = process.env.SECRET;"# 1092 .to_string(); 1093 1094 manager 1095 .open(uri.clone(), "javascript".to_string(), content, 1) 1096 .await; 1097 1098 let graph = manager.get_binding_graph(&uri).unwrap(); 1099 assert_eq!(graph.direct_references().len(), 3); 1100 } 1101 1102 #[tokio::test] 1103 async fn test_destructuring_with_rename() { 1104 let manager = create_test_manager(); 1105 let uri = test_uri("test.js"); 1106 let content = r#"const { DATABASE_URL: dbUrl } = process.env;"#.to_string(); 1107 1108 manager 1109 .open(uri.clone(), "javascript".to_string(), content, 1) 1110 .await; 1111 1112 let binding = manager.get_env_binding_cloned(&uri, Position::new(0, 24)); 1113 assert!(binding.is_some()); 1114 let binding = binding.unwrap(); 1115 assert_eq!(binding.binding_name, "dbUrl"); 1116 assert_eq!(binding.env_var_name, "DATABASE_URL"); 1117 } 1118 1119 // ========================================================================= 1120 // Task 2: EditInfo and Edit Handling Tests 1121 // ========================================================================= 1122 1123 fn make_text_change(range: Option<Range>, text: &str) -> TextDocumentContentChangeEvent { 1124 TextDocumentContentChangeEvent { 1125 range, 1126 range_length: None, 1127 text: text.to_string(), 1128 } 1129 } 1130 1131 #[test] 1132 fn test_edit_info_full_replacement() { 1133 let edit_info = EditInfo::full_replacement(); 1134 assert!(edit_info.is_full_replacement); 1135 assert!(edit_info.range.is_none()); 1136 } 1137 1138 #[test] 1139 fn test_edit_info_incremental() { 1140 let range = Range { 1141 start: Position::new(5, 0), 1142 end: Position::new(10, 20), 1143 }; 1144 let edit_info = EditInfo::incremental(range); 1145 assert!(!edit_info.is_full_replacement); 1146 assert!(edit_info.range.is_some()); 1147 assert_eq!(edit_info.range.unwrap(), range); 1148 } 1149 1150 #[tokio::test] 1151 async fn test_change_with_range_creates_incremental_edit() { 1152 let manager = create_test_manager(); 1153 let uri = test_uri("test.js"); 1154 let content = "const x = 1;\nconst y = 2;\nconst z = 3;".to_string(); 1155 1156 manager 1157 .open(uri.clone(), "javascript".to_string(), content, 1) 1158 .await; 1159 1160 // Change with a range should be treated as incremental 1161 let changes = vec![make_text_change( 1162 Some(Range { 1163 start: Position::new(1, 10), 1164 end: Position::new(1, 11), 1165 }), 1166 "3", 1167 )]; 1168 1169 manager.change(&uri, changes, 2).await; 1170 1171 let doc = manager.get(&uri).unwrap(); 1172 assert_eq!(doc.version, 2); 1173 // The document content should be updated 1174 // Note: The actual content update depends on how the change is applied 1175 } 1176 1177 #[tokio::test] 1178 async fn test_change_with_full_range_replaces_document() { 1179 let manager = create_test_manager(); 1180 let uri = test_uri("test.js"); 1181 let content = "const x = 1;".to_string(); 1182 1183 manager 1184 .open(uri.clone(), "javascript".to_string(), content, 1) 1185 .await; 1186 1187 // Full-document replacement expressed as a ranged incremental change 1188 let new_content = "const y = 2;".to_string(); 1189 let changes = vec![make_text_change( 1190 Some(Range::new(Position::new(0, 0), Position::new(0, 12))), 1191 &new_content, 1192 )]; 1193 1194 manager.change(&uri, changes, 2).await; 1195 1196 let doc = manager.get(&uri).unwrap(); 1197 assert_eq!(doc.version, 2); 1198 assert_eq!(doc.content.as_str(), new_content); 1199 } 1200 1201 #[tokio::test] 1202 async fn test_edit_range_merging_single_edit() { 1203 let manager = create_test_manager(); 1204 let uri = test_uri("test.js"); 1205 let content = "const x = 1;\nconst y = 2;\nconst z = 3;".to_string(); 1206 1207 manager 1208 .open(uri.clone(), "javascript".to_string(), content, 1) 1209 .await; 1210 1211 // Single edit should be preserved as-is 1212 let changes = vec![make_text_change( 1213 Some(Range { 1214 start: Position::new(1, 0), 1215 end: Position::new(1, 12), 1216 }), 1217 "const y = 5;", 1218 )]; 1219 1220 manager.change(&uri, changes, 2).await; 1221 1222 let doc = manager.get(&uri).unwrap(); 1223 assert_eq!(doc.version, 2); 1224 } 1225 1226 #[tokio::test] 1227 async fn test_edit_range_merging_multiple_edits() { 1228 let manager = create_test_manager(); 1229 let uri = test_uri("test.js"); 1230 let content = "line0\nline1\nline2\nline3\nline4".to_string(); 1231 1232 manager 1233 .open(uri.clone(), "javascript".to_string(), content, 1) 1234 .await; 1235 1236 // Multiple non-overlapping edits should be merged into one covering range 1237 let changes = vec![ 1238 make_text_change( 1239 Some(Range { 1240 start: Position::new(1, 0), 1241 end: Position::new(1, 5), 1242 }), 1243 "LINE1", 1244 ), 1245 make_text_change( 1246 Some(Range { 1247 start: Position::new(3, 0), 1248 end: Position::new(3, 5), 1249 }), 1250 "LINE3", 1251 ), 1252 ]; 1253 1254 manager.change(&uri, changes, 2).await; 1255 1256 let doc = manager.get(&uri).unwrap(); 1257 assert_eq!(doc.version, 2); 1258 } 1259 1260 #[tokio::test] 1261 async fn test_edit_range_merging_overlapping_edits() { 1262 let manager = create_test_manager(); 1263 let uri = test_uri("test.js"); 1264 let content = "line0\nline1\nline2\nline3\nline4".to_string(); 1265 1266 manager 1267 .open(uri.clone(), "javascript".to_string(), content, 1) 1268 .await; 1269 1270 // Overlapping edits should be merged correctly 1271 let changes = vec![ 1272 make_text_change( 1273 Some(Range { 1274 start: Position::new(1, 0), 1275 end: Position::new(2, 5), 1276 }), 1277 "MERGED1", 1278 ), 1279 make_text_change( 1280 Some(Range { 1281 start: Position::new(2, 0), 1282 end: Position::new(3, 5), 1283 }), 1284 "MERGED2", 1285 ), 1286 ]; 1287 1288 manager.change(&uri, changes, 2).await; 1289 1290 let doc = manager.get(&uri).unwrap(); 1291 assert_eq!(doc.version, 2); 1292 } 1293 1294 #[tokio::test] 1295 async fn test_edit_range_merging_same_line_different_columns() { 1296 let manager = create_test_manager(); 1297 let uri = test_uri("test.js"); 1298 let content = "const x = 1; const y = 2;".to_string(); 1299 1300 manager 1301 .open(uri.clone(), "javascript".to_string(), content, 1) 1302 .await; 1303 1304 // Edits on the same line with different columns 1305 let changes = vec![ 1306 make_text_change( 1307 Some(Range { 1308 start: Position::new(0, 10), 1309 end: Position::new(0, 11), 1310 }), 1311 "5", 1312 ), 1313 make_text_change( 1314 Some(Range { 1315 start: Position::new(0, 23), 1316 end: Position::new(0, 24), 1317 }), 1318 "10", 1319 ), 1320 ]; 1321 1322 manager.change(&uri, changes, 2).await; 1323 1324 let doc = manager.get(&uri).unwrap(); 1325 assert_eq!(doc.version, 2); 1326 } 1327 1328 #[tokio::test] 1329 async fn test_edit_range_merging_two_ranges_covering_full_doc() { 1330 let manager = create_test_manager(); 1331 let uri = test_uri("test.js"); 1332 let content = "const x = 1;".to_string(); 1333 1334 manager 1335 .open(uri.clone(), "javascript".to_string(), content, 1) 1336 .await; 1337 1338 // A full-range replacement after a smaller edit should overwrite whole document 1339 let new_content = "const y = 2; const z = 3;".to_string(); 1340 let changes = vec![ 1341 make_text_change( 1342 Some(Range { 1343 start: Position::new(0, 10), 1344 end: Position::new(0, 11), 1345 }), 1346 "5", 1347 ), 1348 make_text_change( 1349 Some(Range::new(Position::new(0, 0), Position::new(0, 12))), 1350 &new_content, 1351 ), 1352 ]; 1353 1354 manager.change(&uri, changes, 2).await; 1355 1356 let doc = manager.get(&uri).unwrap(); 1357 assert_eq!(doc.version, 2); 1358 // Full replacement content should be the final content 1359 assert_eq!(doc.content.as_str(), new_content); 1360 } 1361 1362 #[tokio::test] 1363 async fn test_analyze_content_with_edit_full_range_replacement() { 1364 let manager = create_test_manager(); 1365 let uri = test_uri("test.js"); 1366 let content = "const db = process.env.DATABASE_URL;".to_string(); 1367 1368 manager 1369 .open(uri.clone(), "javascript".to_string(), content, 1) 1370 .await; 1371 1372 // Full-range replacement should trigger full analysis 1373 let new_content = "const api = process.env.API_KEY;".to_string(); 1374 let changes = vec![make_text_change( 1375 Some(Range::new(Position::new(0, 0), Position::new(0, 36))), 1376 &new_content, 1377 )]; 1378 1379 manager.change(&uri, changes, 2).await; 1380 1381 let doc = manager.get(&uri).unwrap(); 1382 assert!(doc.tree.is_some()); 1383 1384 // Check that the binding graph reflects the new content 1385 let graph = manager.get_binding_graph(&uri).unwrap(); 1386 assert!(!graph.direct_references().is_empty()); 1387 assert_eq!(graph.direct_references()[0].name, "API_KEY"); 1388 } 1389 1390 #[tokio::test] 1391 async fn test_analyze_content_with_edit_no_old_graph_delegates() { 1392 let manager = create_test_manager(); 1393 let uri = test_uri("test.js"); 1394 1395 // Create a document with unknown language (no binding graph) 1396 let content = "some content".to_string(); 1397 manager 1398 .open(uri.clone(), "unknown".to_string(), content, 1) 1399 .await; 1400 1401 // Then change to JavaScript content - should do full analysis 1402 // First, we need to close and reopen with JavaScript 1403 manager.close(&uri); 1404 1405 let js_content = "const x = process.env.VAR;".to_string(); 1406 manager 1407 .open(uri.clone(), "javascript".to_string(), js_content.clone(), 2) 1408 .await; 1409 1410 let doc = manager.get(&uri).unwrap(); 1411 assert!(doc.tree.is_some()); 1412 1413 let graph = manager.get_binding_graph(&uri).unwrap(); 1414 assert!(!graph.direct_references().is_empty()); 1415 } 1416 1417 #[tokio::test] 1418 async fn test_close_document() { 1419 let manager = create_test_manager(); 1420 let uri = test_uri("test.js"); 1421 let content = "const x = 1;".to_string(); 1422 1423 manager 1424 .open(uri.clone(), "javascript".to_string(), content, 1) 1425 .await; 1426 assert!(manager.get(&uri).is_some()); 1427 1428 manager.close(&uri); 1429 assert!(manager.get(&uri).is_none()); 1430 } 1431 1432 #[tokio::test] 1433 async fn test_document_count() { 1434 let manager = create_test_manager(); 1435 1436 assert_eq!(manager.document_count(), 0); 1437 1438 manager 1439 .open( 1440 test_uri("a.js"), 1441 "javascript".to_string(), 1442 "a".to_string(), 1443 1, 1444 ) 1445 .await; 1446 assert_eq!(manager.document_count(), 1); 1447 1448 manager 1449 .open( 1450 test_uri("b.js"), 1451 "javascript".to_string(), 1452 "b".to_string(), 1453 1, 1454 ) 1455 .await; 1456 assert_eq!(manager.document_count(), 2); 1457 1458 manager.close(&test_uri("a.js")); 1459 assert_eq!(manager.document_count(), 1); 1460 } 1461 1462 #[tokio::test] 1463 async fn test_has_syntax_errors_clean_code() { 1464 let manager = create_test_manager(); 1465 let uri = test_uri("test.js"); 1466 let content = "const x = 1;".to_string(); 1467 1468 manager 1469 .open(uri.clone(), "javascript".to_string(), content, 1) 1470 .await; 1471 1472 let has_errors = manager.has_syntax_errors(&uri); 1473 assert!(has_errors.is_some()); 1474 assert!(!has_errors.unwrap()); 1475 } 1476 1477 #[tokio::test] 1478 async fn test_has_syntax_errors_with_error() { 1479 let manager = create_test_manager(); 1480 let uri = test_uri("test.js"); 1481 // Intentionally broken syntax 1482 let content = "const x = ;".to_string(); 1483 1484 manager 1485 .open(uri.clone(), "javascript".to_string(), content, 1) 1486 .await; 1487 1488 let has_errors = manager.has_syntax_errors(&uri); 1489 assert!(has_errors.is_some()); 1490 assert!(has_errors.unwrap()); 1491 } 1492 1493 #[tokio::test] 1494 async fn test_get_syntax_errors() { 1495 let manager = create_test_manager(); 1496 let uri = test_uri("test.js"); 1497 // Intentionally broken syntax 1498 let content = "const x = ;".to_string(); 1499 1500 manager 1501 .open(uri.clone(), "javascript".to_string(), content, 1) 1502 .await; 1503 1504 let errors = manager.get_syntax_errors(&uri); 1505 assert!(!errors.is_empty()); 1506 } 1507 1508 #[tokio::test] 1509 async fn test_get_syntax_errors_no_errors() { 1510 let manager = create_test_manager(); 1511 let uri = test_uri("test.js"); 1512 let content = "const x = 1;".to_string(); 1513 1514 manager 1515 .open(uri.clone(), "javascript".to_string(), content, 1) 1516 .await; 1517 1518 let errors = manager.get_syntax_errors(&uri); 1519 assert!(errors.is_empty()); 1520 } 1521 1522 #[tokio::test] 1523 async fn test_get_syntax_errors_nonexistent_document() { 1524 let manager = create_test_manager(); 1525 let uri = test_uri("nonexistent.js"); 1526 1527 let errors = manager.get_syntax_errors(&uri); 1528 assert!(errors.is_empty()); 1529 } 1530 }