/ src / analysis / document.rs
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  }