deps_lsp/document/
state.rs

1use dashmap::DashMap;
2use deps_core::HttpCache;
3use deps_core::lockfile::LockFileCache;
4use deps_core::{EcosystemRegistry, ParseResult};
5use std::collections::HashMap;
6
7#[cfg(feature = "cargo")]
8use deps_cargo::{CargoVersion, ParsedDependency};
9#[cfg(feature = "go")]
10use deps_go::{GoDependency, GoVersion};
11#[cfg(feature = "npm")]
12use deps_npm::{NpmDependency, NpmVersion};
13#[cfg(feature = "pypi")]
14use deps_pypi::{PypiDependency, PypiVersion};
15use std::sync::Arc;
16use std::time::{Duration, Instant};
17use tokio::task::JoinHandle;
18use tower_lsp_server::ls_types::Uri;
19
20/// Unified dependency enum for multi-ecosystem support.
21///
22/// Wraps ecosystem-specific dependency types to allow storing
23/// dependencies from different ecosystems in the same document state.
24#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub enum UnifiedDependency {
27    #[cfg(feature = "cargo")]
28    Cargo(ParsedDependency),
29    #[cfg(feature = "npm")]
30    Npm(NpmDependency),
31    #[cfg(feature = "pypi")]
32    Pypi(PypiDependency),
33    #[cfg(feature = "go")]
34    Go(GoDependency),
35}
36
37impl UnifiedDependency {
38    /// Returns the dependency name.
39    #[allow(unreachable_patterns)]
40    pub fn name(&self) -> &str {
41        match self {
42            #[cfg(feature = "cargo")]
43            Self::Cargo(dep) => &dep.name,
44            #[cfg(feature = "npm")]
45            Self::Npm(dep) => &dep.name,
46            #[cfg(feature = "pypi")]
47            Self::Pypi(dep) => &dep.name,
48            #[cfg(feature = "go")]
49            Self::Go(dep) => &dep.module_path,
50            _ => unreachable!("no ecosystem features enabled"),
51        }
52    }
53
54    /// Returns the name range for LSP operations.
55    #[allow(unreachable_patterns)]
56    pub fn name_range(&self) -> tower_lsp_server::ls_types::Range {
57        match self {
58            #[cfg(feature = "cargo")]
59            Self::Cargo(dep) => dep.name_range,
60            #[cfg(feature = "npm")]
61            Self::Npm(dep) => dep.name_range,
62            #[cfg(feature = "pypi")]
63            Self::Pypi(dep) => dep.name_range,
64            #[cfg(feature = "go")]
65            Self::Go(dep) => dep.module_path_range,
66            _ => unreachable!("no ecosystem features enabled"),
67        }
68    }
69
70    /// Returns the version requirement string if present.
71    #[allow(unreachable_patterns)]
72    pub fn version_req(&self) -> Option<&str> {
73        match self {
74            #[cfg(feature = "cargo")]
75            Self::Cargo(dep) => dep.version_req.as_deref(),
76            #[cfg(feature = "npm")]
77            Self::Npm(dep) => dep.version_req.as_deref(),
78            #[cfg(feature = "pypi")]
79            Self::Pypi(dep) => dep.version_req.as_deref(),
80            #[cfg(feature = "go")]
81            Self::Go(dep) => dep.version.as_deref(),
82            _ => unreachable!("no ecosystem features enabled"),
83        }
84    }
85
86    /// Returns the version range for LSP operations if present.
87    #[allow(unreachable_patterns)]
88    pub fn version_range(&self) -> Option<tower_lsp_server::ls_types::Range> {
89        match self {
90            #[cfg(feature = "cargo")]
91            Self::Cargo(dep) => dep.version_range,
92            #[cfg(feature = "npm")]
93            Self::Npm(dep) => dep.version_range,
94            #[cfg(feature = "pypi")]
95            Self::Pypi(dep) => dep.version_range,
96            #[cfg(feature = "go")]
97            Self::Go(dep) => dep.version_range,
98            _ => unreachable!("no ecosystem features enabled"),
99        }
100    }
101
102    /// Returns true if this is a registry dependency (not Git/Path/Workspace/etc).
103    #[allow(unreachable_patterns)]
104    pub fn is_registry(&self) -> bool {
105        use deps_core::Dependency;
106        match self {
107            #[cfg(feature = "cargo")]
108            Self::Cargo(dep) => dep.source().is_registry(),
109            #[cfg(feature = "npm")]
110            Self::Npm(dep) => dep.source().is_registry(),
111            #[cfg(feature = "pypi")]
112            Self::Pypi(dep) => dep.source().is_registry(),
113            #[cfg(feature = "go")]
114            Self::Go(dep) => dep.source().is_registry(),
115            _ => unreachable!("no ecosystem features enabled"),
116        }
117    }
118}
119
120/// Unified version information enum for multi-ecosystem support.
121///
122/// Wraps ecosystem-specific version types.
123#[derive(Debug, Clone)]
124#[non_exhaustive]
125pub enum UnifiedVersion {
126    #[cfg(feature = "cargo")]
127    Cargo(CargoVersion),
128    #[cfg(feature = "npm")]
129    Npm(NpmVersion),
130    #[cfg(feature = "pypi")]
131    Pypi(PypiVersion),
132    #[cfg(feature = "go")]
133    Go(GoVersion),
134}
135
136impl UnifiedVersion {
137    /// Returns the version number as a string.
138    #[allow(unreachable_patterns)]
139    pub fn version_string(&self) -> &str {
140        match self {
141            #[cfg(feature = "cargo")]
142            Self::Cargo(v) => &v.num,
143            #[cfg(feature = "npm")]
144            Self::Npm(v) => &v.version,
145            #[cfg(feature = "pypi")]
146            Self::Pypi(v) => &v.version,
147            #[cfg(feature = "go")]
148            Self::Go(v) => &v.version,
149            _ => unreachable!("no ecosystem features enabled"),
150        }
151    }
152
153    /// Returns true if this version is yanked/deprecated.
154    #[allow(unreachable_patterns)]
155    pub fn is_yanked(&self) -> bool {
156        match self {
157            #[cfg(feature = "cargo")]
158            Self::Cargo(v) => v.yanked,
159            #[cfg(feature = "npm")]
160            Self::Npm(v) => v.deprecated,
161            #[cfg(feature = "pypi")]
162            Self::Pypi(v) => v.yanked,
163            #[cfg(feature = "go")]
164            Self::Go(v) => v.retracted,
165            _ => unreachable!("no ecosystem features enabled"),
166        }
167    }
168}
169
170// Re-export LoadingState from deps-core for convenience
171pub use deps_core::LoadingState;
172
173/// Package ecosystem type.
174///
175/// Identifies which package manager and manifest file format
176/// a document belongs to. Used for routing LSP operations to
177/// the appropriate parser and registry.
178///
179/// # Examples
180///
181/// ```
182/// use deps_lsp::document::Ecosystem;
183///
184/// let cargo = Ecosystem::from_filename("Cargo.toml");
185/// assert_eq!(cargo, Some(Ecosystem::Cargo));
186///
187/// let npm = Ecosystem::from_filename("package.json");
188/// assert_eq!(npm, Some(Ecosystem::Npm));
189///
190/// let pypi = Ecosystem::from_filename("pyproject.toml");
191/// assert_eq!(pypi, Some(Ecosystem::Pypi));
192///
193/// let unknown = Ecosystem::from_filename("requirements.txt");
194/// assert_eq!(unknown, None);
195/// ```
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197#[non_exhaustive]
198pub enum Ecosystem {
199    /// Rust Cargo ecosystem (Cargo.toml)
200    Cargo,
201    /// JavaScript/TypeScript npm ecosystem (package.json)
202    Npm,
203    /// Python PyPI ecosystem (pyproject.toml)
204    Pypi,
205    /// Go modules ecosystem (go.mod)
206    Go,
207}
208
209impl Ecosystem {
210    /// Detects ecosystem from filename.
211    ///
212    /// Returns `Some(Ecosystem)` if the filename matches a known manifest file,
213    /// or `None` if the file is not recognized.
214    pub fn from_filename(filename: &str) -> Option<Self> {
215        match filename {
216            "Cargo.toml" => Some(Self::Cargo),
217            "package.json" => Some(Self::Npm),
218            "pyproject.toml" => Some(Self::Pypi),
219            "go.mod" => Some(Self::Go),
220            _ => None,
221        }
222    }
223
224    /// Detects ecosystem from full URI path.
225    ///
226    /// Extracts the filename from the URI and checks if it matches a known manifest.
227    pub fn from_uri(uri: &Uri) -> Option<Self> {
228        let path = uri.path();
229        let filename = path.as_str().split('/').next_back()?;
230        Self::from_filename(filename)
231    }
232}
233
234/// State for a single open document.
235///
236/// Stores the document content, parsed dependency information, and cached
237/// version data for a single file. The state is updated when the document
238/// changes or when version information is fetched from the registry.
239///
240/// Supports multiple package ecosystems (Cargo, npm) with unified dependency
241/// and version storage.
242///
243/// # Examples
244///
245/// ```no_run
246/// use deps_lsp::document::{DocumentState, Ecosystem, UnifiedDependency};
247/// use deps_lsp::ParsedDependency;
248/// use deps_cargo::{DependencySection, DependencySource};
249/// use tower_lsp_server::ls_types::{Position, Range};
250///
251/// let dep = ParsedDependency {
252///     name: "serde".into(),
253///     name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
254///     version_req: Some("1.0".into()),
255///     version_range: Some(Range::new(Position::new(0, 8), Position::new(0, 12))),
256///     features: vec![],
257///     features_range: None,
258///     source: DependencySource::Registry,
259///     section: DependencySection::Dependencies,
260/// };
261///
262/// let state = DocumentState::new(
263///     Ecosystem::Cargo,
264///     "[dependencies]\nserde = \"1.0\"".into(),
265///     vec![UnifiedDependency::Cargo(dep)],
266/// );
267///
268/// assert!(state.versions.is_empty());
269/// assert_eq!(state.dependencies.len(), 1);
270/// ```
271pub struct DocumentState {
272    /// Package ecosystem type (deprecated, use ecosystem_id)
273    pub ecosystem: Ecosystem,
274    /// Ecosystem identifier ("cargo", "npm", "pypi")
275    pub ecosystem_id: &'static str,
276    /// Original document content
277    pub content: String,
278    /// Parsed dependencies with positions (legacy)
279    pub dependencies: Vec<UnifiedDependency>,
280    /// Parsed result as trait object (new architecture)
281    /// Note: This is not cloned when DocumentState is cloned
282    #[allow(dead_code)]
283    parse_result: Option<Box<dyn ParseResult>>,
284    /// Cached latest version information from registry
285    pub versions: HashMap<String, UnifiedVersion>,
286    /// Simplified cached versions (just strings) for new architecture
287    pub cached_versions: HashMap<String, String>,
288    /// Resolved versions from lock file
289    pub resolved_versions: HashMap<String, String>,
290    /// Last successful parse time
291    pub parsed_at: Instant,
292    /// Current loading state for registry data
293    pub loading_state: LoadingState,
294    /// When the current loading operation started (for timeout/metrics)
295    pub loading_started_at: Option<Instant>,
296}
297
298impl Clone for DocumentState {
299    fn clone(&self) -> Self {
300        Self {
301            ecosystem: self.ecosystem,
302            ecosystem_id: self.ecosystem_id,
303            content: self.content.clone(),
304            dependencies: self.dependencies.clone(),
305            parse_result: None, // Don't clone trait object
306            versions: self.versions.clone(),
307            cached_versions: self.cached_versions.clone(),
308            resolved_versions: self.resolved_versions.clone(),
309            parsed_at: self.parsed_at,
310            loading_state: self.loading_state,
311            // Note: Instant is Copy. Clones share the same loading start time.
312            loading_started_at: self.loading_started_at,
313        }
314    }
315}
316
317/// Tracks recent cold start attempts per URI to prevent DOS.
318///
319/// Uses rate limiting with a configurable minimum interval between
320/// cold start attempts for the same URI. This prevents malicious or
321/// buggy clients from overwhelming the server with rapid file loading
322/// requests.
323///
324/// # Examples
325///
326/// ```
327/// use deps_lsp::document::ColdStartLimiter;
328/// use tower_lsp_server::ls_types::Uri;
329/// use std::time::Duration;
330///
331/// let limiter = ColdStartLimiter::new(Duration::from_millis(100));
332/// let uri = Uri::from_file_path("/test.toml").unwrap();
333///
334/// assert!(limiter.allow_cold_start(&uri));
335/// assert!(!limiter.allow_cold_start(&uri)); // Rate limited
336/// ```
337#[derive(Debug)]
338pub struct ColdStartLimiter {
339    /// Maps URI to last cold start attempt time.
340    last_attempts: DashMap<Uri, Instant>,
341    /// Minimum interval between cold start attempts for the same URI.
342    min_interval: Duration,
343}
344
345impl ColdStartLimiter {
346    /// Creates a new cold start limiter with the specified minimum interval.
347    pub fn new(min_interval: Duration) -> Self {
348        Self {
349            last_attempts: DashMap::new(),
350            min_interval,
351        }
352    }
353
354    /// Returns true if cold start is allowed, false if rate limited.
355    ///
356    /// Updates the last attempt time if the cold start is allowed.
357    pub fn allow_cold_start(&self, uri: &Uri) -> bool {
358        let now = Instant::now();
359
360        // Check last attempt time
361        if let Some(mut entry) = self.last_attempts.get_mut(uri) {
362            let elapsed = now.duration_since(*entry);
363            if elapsed < self.min_interval {
364                let retry_after = self.min_interval.checked_sub(elapsed).unwrap();
365                tracing::warn!(
366                    "Cold start rate limited for {:?} (retry after {:?})",
367                    uri,
368                    retry_after
369                );
370                return false;
371            }
372            *entry = now;
373        } else {
374            self.last_attempts.insert(uri.clone(), now);
375        }
376
377        true
378    }
379
380    /// Cleans up old entries periodically.
381    ///
382    /// Removes entries older than `max_age` to prevent unbounded memory growth.
383    /// Should be called from a background task.
384    pub fn cleanup_old_entries(&self, max_age: Duration) {
385        let now = Instant::now();
386        self.last_attempts
387            .retain(|_, instant| now.duration_since(*instant) < max_age);
388    }
389
390    /// Returns the number of tracked URIs.
391    #[cfg(test)]
392    pub fn tracked_count(&self) -> usize {
393        self.last_attempts.len()
394    }
395}
396
397impl std::fmt::Debug for DocumentState {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
399        f.debug_struct("DocumentState")
400            .field("ecosystem", &self.ecosystem)
401            .field("ecosystem_id", &self.ecosystem_id)
402            .field("content_len", &self.content.len())
403            .field("dependencies_count", &self.dependencies.len())
404            .field("has_parse_result", &self.parse_result.is_some())
405            .field("versions_count", &self.versions.len())
406            .field("cached_versions_count", &self.cached_versions.len())
407            .field("resolved_versions_count", &self.resolved_versions.len())
408            .field("parsed_at", &self.parsed_at)
409            .field("loading_state", &self.loading_state)
410            .field("loading_started_at", &self.loading_started_at)
411            .finish()
412    }
413}
414
415impl DocumentState {
416    /// Creates a new document state (legacy constructor).
417    ///
418    /// Initializes with the given ecosystem, content, and parsed dependencies.
419    /// Version information starts empty and is populated asynchronously.
420    pub fn new(
421        ecosystem: Ecosystem,
422        content: String,
423        dependencies: Vec<UnifiedDependency>,
424    ) -> Self {
425        let ecosystem_id = match ecosystem {
426            Ecosystem::Cargo => "cargo",
427            Ecosystem::Npm => "npm",
428            Ecosystem::Pypi => "pypi",
429            Ecosystem::Go => "go",
430        };
431
432        Self {
433            ecosystem,
434            ecosystem_id,
435            content,
436            dependencies,
437            parse_result: None,
438            versions: HashMap::new(),
439            cached_versions: HashMap::new(),
440            resolved_versions: HashMap::new(),
441            parsed_at: Instant::now(),
442            loading_state: LoadingState::Idle,
443            loading_started_at: None,
444        }
445    }
446
447    /// Creates a new document state using trait objects (new architecture).
448    ///
449    /// This is the preferred constructor for Phase 3+ implementations.
450    pub fn new_from_parse_result(
451        ecosystem_id: &'static str,
452        content: String,
453        parse_result: Box<dyn ParseResult>,
454    ) -> Self {
455        let ecosystem = match ecosystem_id {
456            "cargo" => Ecosystem::Cargo,
457            "npm" => Ecosystem::Npm,
458            "pypi" => Ecosystem::Pypi,
459            "go" => Ecosystem::Go,
460            _ => Ecosystem::Cargo, // Default fallback
461        };
462
463        Self {
464            ecosystem,
465            ecosystem_id,
466            content,
467            dependencies: vec![],
468            parse_result: Some(parse_result),
469            versions: HashMap::new(),
470            cached_versions: HashMap::new(),
471            resolved_versions: HashMap::new(),
472            parsed_at: Instant::now(),
473            loading_state: LoadingState::Idle,
474            loading_started_at: None,
475        }
476    }
477
478    /// Creates a new document state without a parse result.
479    ///
480    /// Used when parsing fails but the document should still be stored
481    /// to enable fallback completion and other LSP features.
482    pub fn new_without_parse_result(ecosystem_id: &'static str, content: String) -> Self {
483        let ecosystem = match ecosystem_id {
484            "cargo" => Ecosystem::Cargo,
485            "npm" => Ecosystem::Npm,
486            "pypi" => Ecosystem::Pypi,
487            "go" => Ecosystem::Go,
488            _ => Ecosystem::Cargo, // Default fallback
489        };
490
491        Self {
492            ecosystem,
493            ecosystem_id,
494            content,
495            dependencies: vec![],
496            parse_result: None,
497            versions: HashMap::new(),
498            cached_versions: HashMap::new(),
499            resolved_versions: HashMap::new(),
500            parsed_at: Instant::now(),
501            loading_state: LoadingState::Idle,
502            loading_started_at: None,
503        }
504    }
505
506    /// Gets a reference to the parse result if available.
507    pub fn parse_result(&self) -> Option<&dyn ParseResult> {
508        self.parse_result.as_ref().map(std::convert::AsRef::as_ref)
509    }
510
511    /// Updates the cached latest version information for dependencies.
512    pub fn update_versions(&mut self, versions: HashMap<String, UnifiedVersion>) {
513        self.versions = versions;
514    }
515
516    /// Updates the simplified cached versions (new architecture).
517    pub fn update_cached_versions(&mut self, versions: HashMap<String, String>) {
518        self.cached_versions = versions;
519    }
520
521    /// Updates the resolved versions from lock file.
522    pub fn update_resolved_versions(&mut self, versions: HashMap<String, String>) {
523        self.resolved_versions = versions;
524    }
525
526    /// Mark document as loading registry data.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// use deps_lsp::document::DocumentState;
532    ///
533    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
534    /// doc.set_loading();
535    /// assert!(doc.loading_started_at.is_some());
536    /// ```
537    ///
538    /// # Thread Safety
539    ///
540    /// This method requires exclusive access (`&mut self`). When used with
541    /// `DashMap::get_mut()`, thread safety is guaranteed by the lock.
542    /// Calling while already `Loading` resets the timer.
543    pub fn set_loading(&mut self) {
544        self.loading_state = LoadingState::Loading;
545        self.loading_started_at = Some(Instant::now());
546    }
547
548    /// Mark document as loaded with fresh data.
549    ///
550    /// # Examples
551    ///
552    /// ```
553    /// use deps_lsp::document::{DocumentState, LoadingState};
554    ///
555    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
556    /// doc.set_loading();
557    /// doc.set_loaded();
558    /// assert_eq!(doc.loading_state, LoadingState::Loaded);
559    /// assert!(doc.loading_started_at.is_none());
560    /// ```
561    pub fn set_loaded(&mut self) {
562        self.loading_state = LoadingState::Loaded;
563        self.loading_started_at = None;
564    }
565
566    /// Mark document as failed to load (keeps old cached data).
567    ///
568    /// # Examples
569    ///
570    /// ```
571    /// use deps_lsp::document::{DocumentState, LoadingState};
572    ///
573    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
574    /// doc.set_loading();
575    /// doc.set_failed();
576    /// assert_eq!(doc.loading_state, LoadingState::Failed);
577    /// assert!(doc.loading_started_at.is_none());
578    /// ```
579    pub fn set_failed(&mut self) {
580        self.loading_state = LoadingState::Failed;
581        self.loading_started_at = None;
582    }
583
584    /// Get current loading duration if loading.
585    ///
586    /// Returns `None` if not currently loading, or `Some(Duration)` representing
587    /// how long the current loading operation has been running.
588    ///
589    /// # Examples
590    ///
591    /// ```
592    /// use deps_lsp::document::DocumentState;
593    ///
594    /// let mut doc = DocumentState::new_without_parse_result("cargo", "".into());
595    /// assert!(doc.loading_duration().is_none());
596    ///
597    /// doc.set_loading();
598    /// assert!(doc.loading_duration().is_some());
599    /// ```
600    #[must_use]
601    pub fn loading_duration(&self) -> Option<Duration> {
602        self.loading_started_at
603            .map(|start| Instant::now().duration_since(start))
604    }
605}
606
607/// Global LSP server state.
608///
609/// Manages all open documents, HTTP cache, lock file cache, and background
610/// tasks for the server. This state is shared across all LSP handlers via
611/// `Arc` and uses concurrent data structures (`DashMap`, `RwLock`) for
612/// thread-safe access.
613///
614/// # Examples
615///
616/// ```
617/// use deps_lsp::document::ServerState;
618/// use tower_lsp_server::ls_types::Uri;
619///
620/// let state = ServerState::new();
621/// assert_eq!(state.document_count(), 0);
622/// ```
623pub struct ServerState {
624    /// Open documents by URI
625    pub documents: DashMap<Uri, DocumentState>,
626    /// HTTP cache for registry requests
627    pub cache: Arc<HttpCache>,
628    /// Lock file cache for parsed lock files
629    pub lockfile_cache: Arc<LockFileCache>,
630    /// Ecosystem registry for trait-based architecture
631    pub ecosystem_registry: Arc<EcosystemRegistry>,
632    /// Cold start rate limiter
633    pub cold_start_limiter: ColdStartLimiter,
634    /// Background task handles
635    tasks: tokio::sync::RwLock<HashMap<Uri, JoinHandle<()>>>,
636}
637
638impl ServerState {
639    /// Creates a new server state with default configuration.
640    pub fn new() -> Self {
641        let cache = Arc::new(HttpCache::new());
642        let lockfile_cache = Arc::new(LockFileCache::new());
643        let ecosystem_registry = Arc::new(EcosystemRegistry::new());
644
645        // Register ecosystems based on enabled features
646        crate::register_ecosystems(&ecosystem_registry, Arc::clone(&cache));
647
648        // Create cold start limiter with default 100ms interval (10 req/sec per URI)
649        let cold_start_limiter = ColdStartLimiter::new(Duration::from_millis(100));
650
651        Self {
652            documents: DashMap::new(),
653            cache,
654            lockfile_cache,
655            ecosystem_registry,
656            cold_start_limiter,
657            tasks: tokio::sync::RwLock::new(HashMap::new()),
658        }
659    }
660
661    /// Retrieves document state by URI.
662    ///
663    /// Returns a read-only reference to the document state if it exists.
664    /// The reference holds a lock on the internal map, so it should be
665    /// dropped as soon as possible.
666    pub fn get_document(
667        &self,
668        uri: &Uri,
669    ) -> Option<dashmap::mapref::one::Ref<'_, Uri, DocumentState>> {
670        self.documents.get(uri)
671    }
672
673    /// Retrieves a cloned copy of document state by URI.
674    ///
675    /// This method clones the document state immediately and releases
676    /// the DashMap lock, allowing concurrent access to the map while
677    /// the document is being processed. Use this in hot paths where
678    /// async operations are performed with the document data.
679    ///
680    /// # Performance
681    ///
682    /// Cloning `DocumentState` is relatively cheap as it only clones
683    /// `String` and `HashMap` metadata, not the underlying parse result
684    /// trait object.
685    ///
686    /// # Examples
687    ///
688    /// ```no_run
689    /// # use deps_lsp::document::ServerState;
690    /// # use tower_lsp_server::ls_types::Uri;
691    /// # async fn example(state: &ServerState, uri: &Uri) {
692    /// // Lock released immediately after clone
693    /// let doc = state.get_document_clone(uri);
694    ///
695    /// if let Some(doc) = doc {
696    ///     // Perform async operations without holding lock
697    ///     let result = process_async(&doc).await;
698    /// }
699    /// # }
700    /// # async fn process_async(doc: &deps_lsp::document::DocumentState) {}
701    /// ```
702    pub fn get_document_clone(&self, uri: &Uri) -> Option<DocumentState> {
703        self.documents.get(uri).map(|doc| doc.clone())
704    }
705
706    /// Updates or inserts document state.
707    ///
708    /// If a document already exists at the given URI, it is replaced.
709    /// Otherwise, a new entry is created.
710    pub fn update_document(&self, uri: Uri, state: DocumentState) {
711        self.documents.insert(uri, state);
712    }
713
714    /// Removes document state and returns the removed entry.
715    ///
716    /// Returns `None` if no document exists at the given URI.
717    pub fn remove_document(&self, uri: &Uri) -> Option<(Uri, DocumentState)> {
718        self.documents.remove(uri)
719    }
720
721    /// Spawns a background task for a document.
722    ///
723    /// If a task already exists for the given URI, it is aborted before
724    /// the new task is registered. This ensures only one background task
725    /// runs per document.
726    ///
727    /// Typical use case: fetching version data asynchronously after
728    /// document open or change.
729    pub async fn spawn_background_task(&self, uri: Uri, task: JoinHandle<()>) {
730        let mut tasks = self.tasks.write().await;
731
732        // Cancel existing task if any
733        if let Some(old_task) = tasks.remove(&uri) {
734            old_task.abort();
735        }
736
737        tasks.insert(uri, task);
738    }
739
740    /// Cancels the background task for a document.
741    ///
742    /// If no task exists, this is a no-op.
743    pub async fn cancel_background_task(&self, uri: &Uri) {
744        let mut tasks = self.tasks.write().await;
745        if let Some(task) = tasks.remove(uri) {
746            task.abort();
747        }
748    }
749
750    /// Returns the number of open documents.
751    pub fn document_count(&self) -> usize {
752        self.documents.len()
753    }
754}
755
756impl Default for ServerState {
757    fn default() -> Self {
758        Self::new()
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    // =========================================================================
767    // Generic tests (no feature flag required)
768    // =========================================================================
769
770    // =========================================================================
771    // LoadingState tests
772    // =========================================================================
773
774    mod loading_state_tests {
775        use super::*;
776
777        #[test]
778        fn test_loading_state_default() {
779            let state = LoadingState::default();
780            assert_eq!(state, LoadingState::Idle);
781        }
782
783        #[test]
784        fn test_loading_state_transitions() {
785            use std::time::Duration;
786
787            let content = "[dependencies]\nserde = \"1.0\"".to_string();
788            let mut doc = DocumentState::new_without_parse_result("cargo", content);
789
790            // Initial state
791            assert_eq!(doc.loading_state, LoadingState::Idle);
792            assert!(doc.loading_started_at.is_none());
793
794            // Transition to Loading
795            doc.set_loading();
796            assert_eq!(doc.loading_state, LoadingState::Loading);
797            assert!(doc.loading_started_at.is_some());
798
799            // Small sleep to ensure duration is non-zero
800            std::thread::sleep(Duration::from_millis(10));
801
802            // Check loading duration
803            let duration = doc.loading_duration();
804            assert!(duration.is_some());
805            assert!(duration.unwrap() >= Duration::from_millis(10));
806
807            // Transition to Loaded
808            doc.set_loaded();
809            assert_eq!(doc.loading_state, LoadingState::Loaded);
810            assert!(doc.loading_started_at.is_none());
811            assert!(doc.loading_duration().is_none());
812        }
813
814        #[test]
815        fn test_loading_state_failed_transition() {
816            let content = "[dependencies]\nserde = \"1.0\"".to_string();
817            let mut doc = DocumentState::new_without_parse_result("cargo", content);
818
819            doc.set_loading();
820            assert_eq!(doc.loading_state, LoadingState::Loading);
821
822            doc.set_failed();
823            assert_eq!(doc.loading_state, LoadingState::Failed);
824            assert!(doc.loading_started_at.is_none());
825        }
826
827        #[test]
828        fn test_loading_state_clone() {
829            let content = "[dependencies]\nserde = \"1.0\"".to_string();
830            let mut doc = DocumentState::new_without_parse_result("cargo", content);
831
832            doc.set_loading();
833            let cloned = doc.clone();
834
835            assert_eq!(cloned.loading_state, LoadingState::Loading);
836            assert!(cloned.loading_started_at.is_some());
837        }
838
839        #[test]
840        fn test_loading_state_debug() {
841            let content = "[dependencies]\nserde = \"1.0\"".to_string();
842            let mut doc = DocumentState::new_without_parse_result("cargo", content);
843            doc.set_loading();
844
845            let debug_str = format!("{:?}", doc);
846            assert!(debug_str.contains("loading_state"));
847            assert!(debug_str.contains("Loading"));
848        }
849
850        #[test]
851        fn test_loading_duration_none_when_idle() {
852            let content = "[dependencies]\nserde = \"1.0\"".to_string();
853            let doc = DocumentState::new_without_parse_result("cargo", content);
854
855            assert_eq!(doc.loading_state, LoadingState::Idle);
856            assert!(doc.loading_duration().is_none());
857        }
858
859        #[test]
860        fn test_loading_state_equality() {
861            assert_eq!(LoadingState::Idle, LoadingState::Idle);
862            assert_eq!(LoadingState::Loading, LoadingState::Loading);
863            assert_eq!(LoadingState::Loaded, LoadingState::Loaded);
864            assert_eq!(LoadingState::Failed, LoadingState::Failed);
865
866            assert_ne!(LoadingState::Idle, LoadingState::Loading);
867            assert_ne!(LoadingState::Loading, LoadingState::Loaded);
868        }
869
870        #[test]
871        fn test_loading_duration_tracks_time_correctly() {
872            use std::time::Duration;
873
874            let content = "[dependencies]\nserde = \"1.0\"".to_string();
875            let mut doc = DocumentState::new_without_parse_result("cargo", content);
876
877            doc.set_loading();
878
879            // Check duration increases over time
880            let duration1 = doc.loading_duration().unwrap();
881            std::thread::sleep(Duration::from_millis(20));
882            let duration2 = doc.loading_duration().unwrap();
883
884            assert!(duration2 > duration1, "Duration should increase over time");
885        }
886
887        #[tokio::test]
888        async fn test_concurrent_loading_state_mutations() {
889            use std::sync::Arc;
890            use tokio::sync::Barrier;
891
892            let state = Arc::new(ServerState::new());
893            let uri = Uri::from_file_path("/concurrent-loading-test.toml").unwrap();
894
895            let doc = DocumentState::new_without_parse_result("cargo", String::new());
896            state.update_document(uri.clone(), doc);
897
898            let barrier = Arc::new(Barrier::new(10));
899            let mut handles = vec![];
900
901            for i in 0..10 {
902                let state_clone = Arc::clone(&state);
903                let uri_clone = uri.clone();
904                let barrier_clone = Arc::clone(&barrier);
905
906                handles.push(tokio::spawn(async move {
907                    barrier_clone.wait().await;
908                    if let Some(mut doc) = state_clone.documents.get_mut(&uri_clone) {
909                        if i % 3 == 0 {
910                            doc.set_loading();
911                        } else if i % 3 == 1 {
912                            doc.set_loaded();
913                        } else {
914                            doc.set_failed();
915                        }
916                    }
917                }));
918            }
919
920            for handle in handles {
921                handle.await.unwrap();
922            }
923
924            let doc = state.get_document(&uri).unwrap();
925            assert!(matches!(
926                doc.loading_state,
927                LoadingState::Idle
928                    | LoadingState::Loading
929                    | LoadingState::Loaded
930                    | LoadingState::Failed
931            ));
932        }
933
934        #[test]
935        fn test_set_loaded_idempotent() {
936            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
937
938            doc.set_loading();
939            doc.set_loaded();
940
941            // Call again - should be safe
942            doc.set_loaded();
943
944            assert_eq!(doc.loading_state, LoadingState::Loaded);
945            assert!(doc.loading_started_at.is_none());
946        }
947
948        #[test]
949        fn test_set_loading_resets_timer() {
950            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
951
952            doc.set_loading();
953            let first_start = doc.loading_started_at.unwrap();
954
955            std::thread::sleep(std::time::Duration::from_millis(10));
956
957            // Call set_loading again - should reset timer
958            doc.set_loading();
959            let second_start = doc.loading_started_at.unwrap();
960
961            assert!(second_start > first_start, "Timer should be reset");
962            assert_eq!(doc.loading_state, LoadingState::Loading);
963        }
964
965        #[test]
966        fn test_retry_after_failure() {
967            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
968
969            doc.set_loading();
970            doc.set_failed();
971            assert_eq!(doc.loading_state, LoadingState::Failed);
972            assert!(doc.loading_started_at.is_none());
973
974            // Retry
975            doc.set_loading();
976            assert_eq!(doc.loading_state, LoadingState::Loading);
977            assert!(doc.loading_started_at.is_some());
978
979            doc.set_loaded();
980            assert_eq!(doc.loading_state, LoadingState::Loaded);
981        }
982
983        #[test]
984        fn test_refresh_after_loaded() {
985            let mut doc = DocumentState::new_without_parse_result("cargo", String::new());
986
987            doc.set_loading();
988            doc.set_loaded();
989            assert_eq!(doc.loading_state, LoadingState::Loaded);
990
991            // Refresh
992            doc.set_loading();
993            assert_eq!(doc.loading_state, LoadingState::Loading);
994            assert!(doc.loading_started_at.is_some());
995
996            doc.set_loaded();
997            assert_eq!(doc.loading_state, LoadingState::Loaded);
998        }
999    }
1000
1001    #[test]
1002    fn test_ecosystem_from_filename() {
1003        #[cfg(feature = "cargo")]
1004        assert_eq!(
1005            Ecosystem::from_filename("Cargo.toml"),
1006            Some(Ecosystem::Cargo)
1007        );
1008        #[cfg(feature = "npm")]
1009        assert_eq!(
1010            Ecosystem::from_filename("package.json"),
1011            Some(Ecosystem::Npm)
1012        );
1013        #[cfg(feature = "pypi")]
1014        assert_eq!(
1015            Ecosystem::from_filename("pyproject.toml"),
1016            Some(Ecosystem::Pypi)
1017        );
1018        #[cfg(feature = "go")]
1019        assert_eq!(Ecosystem::from_filename("go.mod"), Some(Ecosystem::Go));
1020        assert_eq!(Ecosystem::from_filename("unknown.txt"), None);
1021    }
1022
1023    #[test]
1024    fn test_ecosystem_from_uri() {
1025        #[cfg(feature = "cargo")]
1026        {
1027            let cargo_uri = Uri::from_file_path("/path/to/Cargo.toml").unwrap();
1028            assert_eq!(Ecosystem::from_uri(&cargo_uri), Some(Ecosystem::Cargo));
1029        }
1030        #[cfg(feature = "npm")]
1031        {
1032            let npm_uri = Uri::from_file_path("/path/to/package.json").unwrap();
1033            assert_eq!(Ecosystem::from_uri(&npm_uri), Some(Ecosystem::Npm));
1034        }
1035        #[cfg(feature = "pypi")]
1036        {
1037            let pypi_uri = Uri::from_file_path("/path/to/pyproject.toml").unwrap();
1038            assert_eq!(Ecosystem::from_uri(&pypi_uri), Some(Ecosystem::Pypi));
1039        }
1040        #[cfg(feature = "go")]
1041        {
1042            let go_uri = Uri::from_file_path("/path/to/go.mod").unwrap();
1043            assert_eq!(Ecosystem::from_uri(&go_uri), Some(Ecosystem::Go));
1044        }
1045        let unknown_uri = Uri::from_file_path("/path/to/README.md").unwrap();
1046        assert_eq!(Ecosystem::from_uri(&unknown_uri), None);
1047    }
1048
1049    #[test]
1050    fn test_ecosystem_from_filename_edge_cases() {
1051        assert_eq!(Ecosystem::from_filename(""), None);
1052        assert_eq!(Ecosystem::from_filename("cargo.toml"), None);
1053        assert_eq!(Ecosystem::from_filename("CARGO.TOML"), None);
1054        assert_eq!(Ecosystem::from_filename("requirements.txt"), None);
1055    }
1056
1057    #[test]
1058    fn test_server_state_creation() {
1059        let state = ServerState::new();
1060        assert_eq!(state.document_count(), 0);
1061        assert!(state.cache.is_empty(), "Cache should start empty");
1062    }
1063
1064    #[test]
1065    fn test_server_state_default() {
1066        let state = ServerState::default();
1067        assert_eq!(state.document_count(), 0);
1068    }
1069
1070    #[tokio::test]
1071    async fn test_server_state_background_tasks() {
1072        let state = ServerState::new();
1073        let uri = Uri::from_file_path("/test.toml").unwrap();
1074
1075        let task = tokio::spawn(async {
1076            tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1077        });
1078
1079        state.spawn_background_task(uri.clone(), task).await;
1080        state.cancel_background_task(&uri).await;
1081    }
1082
1083    #[tokio::test]
1084    async fn test_spawn_background_task_cancels_previous() {
1085        let state = ServerState::new();
1086        let uri = Uri::from_file_path("/test.toml").unwrap();
1087
1088        let task1 = tokio::spawn(async {
1089            tokio::time::sleep(std::time::Duration::from_secs(10)).await;
1090        });
1091        state.spawn_background_task(uri.clone(), task1).await;
1092
1093        let task2 = tokio::spawn(async {
1094            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
1095        });
1096        state.spawn_background_task(uri.clone(), task2).await;
1097        state.cancel_background_task(&uri).await;
1098    }
1099
1100    #[tokio::test]
1101    async fn test_cancel_background_task_nonexistent() {
1102        let state = ServerState::new();
1103        let uri = Uri::from_file_path("/test.toml").unwrap();
1104        state.cancel_background_task(&uri).await;
1105    }
1106
1107    // =========================================================================
1108    // ColdStartLimiter tests
1109    // =========================================================================
1110
1111    mod cold_start_limiter {
1112        use super::*;
1113        use std::time::Duration;
1114
1115        #[test]
1116        fn test_allows_first_request() {
1117            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1118            let uri = Uri::from_file_path("/test.toml").unwrap();
1119            assert!(
1120                limiter.allow_cold_start(&uri),
1121                "First request should be allowed"
1122            );
1123        }
1124
1125        #[test]
1126        fn test_blocks_rapid_requests() {
1127            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1128            let uri = Uri::from_file_path("/test.toml").unwrap();
1129
1130            assert!(limiter.allow_cold_start(&uri), "First request allowed");
1131            assert!(
1132                !limiter.allow_cold_start(&uri),
1133                "Second immediate request should be blocked"
1134            );
1135        }
1136
1137        #[tokio::test]
1138        async fn test_allows_after_interval() {
1139            let limiter = ColdStartLimiter::new(Duration::from_millis(50));
1140            let uri = Uri::from_file_path("/test.toml").unwrap();
1141
1142            assert!(limiter.allow_cold_start(&uri), "First request allowed");
1143            tokio::time::sleep(Duration::from_millis(60)).await;
1144            assert!(
1145                limiter.allow_cold_start(&uri),
1146                "Request after interval should be allowed"
1147            );
1148        }
1149
1150        #[test]
1151        fn test_different_uris_independent() {
1152            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1153            let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1154            let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1155
1156            assert!(limiter.allow_cold_start(&uri1), "URI 1 first request");
1157            assert!(limiter.allow_cold_start(&uri2), "URI 2 first request");
1158            assert!(
1159                !limiter.allow_cold_start(&uri1),
1160                "URI 1 second request blocked"
1161            );
1162            assert!(
1163                !limiter.allow_cold_start(&uri2),
1164                "URI 2 second request blocked"
1165            );
1166        }
1167
1168        #[test]
1169        fn test_cleanup() {
1170            let limiter = ColdStartLimiter::new(Duration::from_millis(100));
1171            let uri1 = Uri::from_file_path("/test1.toml").unwrap();
1172            let uri2 = Uri::from_file_path("/test2.toml").unwrap();
1173
1174            limiter.allow_cold_start(&uri1);
1175            limiter.allow_cold_start(&uri2);
1176            assert_eq!(limiter.tracked_count(), 2, "Should track 2 URIs");
1177
1178            limiter.cleanup_old_entries(Duration::from_millis(0));
1179            assert_eq!(
1180                limiter.tracked_count(),
1181                0,
1182                "All entries should be cleaned up"
1183            );
1184        }
1185
1186        #[tokio::test]
1187        async fn test_concurrent_access() {
1188            use std::sync::Arc;
1189
1190            let limiter = Arc::new(ColdStartLimiter::new(Duration::from_millis(100)));
1191            let uri = Uri::from_file_path("/concurrent-test.toml").unwrap();
1192
1193            let mut handles = vec![];
1194            const CONCURRENT_TASKS: usize = 10;
1195
1196            for _ in 0..CONCURRENT_TASKS {
1197                let limiter_clone = Arc::clone(&limiter);
1198                let uri_clone = uri.clone();
1199                let handle =
1200                    tokio::spawn(async move { limiter_clone.allow_cold_start(&uri_clone) });
1201                handles.push(handle);
1202            }
1203
1204            let mut results = vec![];
1205            for handle in handles {
1206                results.push(handle.await.unwrap());
1207            }
1208
1209            let allowed_count = results.iter().filter(|&&allowed| allowed).count();
1210            assert_eq!(allowed_count, 1, "Exactly one concurrent request allowed");
1211
1212            let blocked_count = results.iter().filter(|&&allowed| !allowed).count();
1213            assert_eq!(
1214                blocked_count,
1215                CONCURRENT_TASKS - 1,
1216                "Rest should be blocked"
1217            );
1218        }
1219    }
1220
1221    // =========================================================================
1222    // Cargo ecosystem tests
1223    // =========================================================================
1224
1225    #[cfg(feature = "cargo")]
1226    mod cargo_tests {
1227        use super::*;
1228        use deps_cargo::{DependencySection, DependencySource};
1229        use tower_lsp_server::ls_types::{Position, Range};
1230
1231        fn create_test_dependency() -> UnifiedDependency {
1232            UnifiedDependency::Cargo(ParsedDependency {
1233                name: "serde".into(),
1234                name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1235                version_req: Some("1.0".into()),
1236                version_range: Some(Range::new(Position::new(0, 9), Position::new(0, 14))),
1237                features: vec![],
1238                features_range: None,
1239                source: DependencySource::Registry,
1240                section: DependencySection::Dependencies,
1241            })
1242        }
1243
1244        #[test]
1245        fn test_document_state_creation() {
1246            let deps = vec![create_test_dependency()];
1247            let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1248
1249            assert_eq!(state.ecosystem, Ecosystem::Cargo);
1250            assert_eq!(state.content, "test content");
1251            assert_eq!(state.dependencies.len(), 1);
1252            assert!(state.versions.is_empty());
1253        }
1254
1255        #[test]
1256        fn test_document_state_update_versions() {
1257            let deps = vec![create_test_dependency()];
1258            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1259
1260            let mut versions = HashMap::new();
1261            versions.insert(
1262                "serde".into(),
1263                UnifiedVersion::Cargo(CargoVersion {
1264                    num: "1.0.0".into(),
1265                    yanked: false,
1266                    features: HashMap::new(),
1267                }),
1268            );
1269
1270            state.update_versions(versions);
1271            assert_eq!(state.versions.len(), 1);
1272            assert!(state.versions.contains_key("serde"));
1273        }
1274
1275        #[test]
1276        fn test_server_state_document_operations() {
1277            let state = ServerState::new();
1278            let uri = Uri::from_file_path("/test.toml").unwrap();
1279            let deps = vec![create_test_dependency()];
1280            let doc_state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1281
1282            state.update_document(uri.clone(), doc_state);
1283            assert_eq!(state.document_count(), 1);
1284
1285            let retrieved = state.get_document(&uri);
1286            assert!(retrieved.is_some());
1287            assert_eq!(retrieved.unwrap().content, "test");
1288
1289            let removed = state.remove_document(&uri);
1290            assert!(removed.is_some());
1291            assert_eq!(state.document_count(), 0);
1292        }
1293
1294        #[test]
1295        fn test_unified_dependency_name() {
1296            let cargo_dep = create_test_dependency();
1297            assert_eq!(cargo_dep.name(), "serde");
1298            assert_eq!(cargo_dep.version_req(), Some("1.0"));
1299            assert!(cargo_dep.is_registry());
1300        }
1301
1302        #[test]
1303        fn test_unified_dependency_git_source() {
1304            let git_dep = UnifiedDependency::Cargo(ParsedDependency {
1305                name: "custom".into(),
1306                name_range: Range::new(Position::new(0, 0), Position::new(0, 6)),
1307                version_req: None,
1308                version_range: None,
1309                features: vec![],
1310                features_range: None,
1311                source: DependencySource::Git {
1312                    url: "https://github.com/user/repo".into(),
1313                    rev: None,
1314                },
1315                section: DependencySection::Dependencies,
1316            });
1317            assert!(!git_dep.is_registry());
1318        }
1319
1320        #[test]
1321        fn test_unified_dependency_workspace_source() {
1322            let ws_dep = UnifiedDependency::Cargo(ParsedDependency {
1323                name: "serde".into(),
1324                name_range: Range::new(Position::new(0, 0), Position::new(0, 5)),
1325                version_req: None,
1326                version_range: None,
1327                features: vec![],
1328                features_range: None,
1329                source: DependencySource::Workspace,
1330                section: DependencySection::Dependencies,
1331            });
1332            assert!(!ws_dep.is_registry());
1333        }
1334
1335        #[test]
1336        fn test_unified_version() {
1337            let version = UnifiedVersion::Cargo(CargoVersion {
1338                num: "1.0.0".into(),
1339                yanked: false,
1340                features: HashMap::new(),
1341            });
1342            assert_eq!(version.version_string(), "1.0.0");
1343            assert!(!version.is_yanked());
1344        }
1345
1346        #[test]
1347        fn test_document_state_new_from_parse_result() {
1348            let state = ServerState::new();
1349            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
1350            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
1351            let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1352
1353            let parse_result = tokio::runtime::Runtime::new()
1354                .unwrap()
1355                .block_on(ecosystem.parse_manifest(&content, &uri))
1356                .unwrap();
1357
1358            let doc_state =
1359                DocumentState::new_from_parse_result("cargo", content.clone(), parse_result);
1360
1361            assert_eq!(doc_state.ecosystem_id, "cargo");
1362            assert_eq!(doc_state.content, content);
1363            assert!(doc_state.parse_result.is_some());
1364        }
1365
1366        #[test]
1367        fn test_document_state_new_without_parse_result() {
1368            let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
1369            let doc_state = DocumentState::new_without_parse_result("cargo", content);
1370
1371            assert_eq!(doc_state.ecosystem_id, "cargo");
1372            assert_eq!(doc_state.ecosystem, Ecosystem::Cargo);
1373            assert!(doc_state.parse_result.is_none());
1374            assert!(doc_state.dependencies.is_empty());
1375        }
1376
1377        #[test]
1378        fn test_document_state_update_resolved_versions() {
1379            let deps = vec![create_test_dependency()];
1380            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1381
1382            let mut resolved = HashMap::new();
1383            resolved.insert("serde".into(), "1.0.195".into());
1384
1385            state.update_resolved_versions(resolved);
1386            assert_eq!(state.resolved_versions.len(), 1);
1387            assert_eq!(
1388                state.resolved_versions.get("serde"),
1389                Some(&"1.0.195".into())
1390            );
1391        }
1392
1393        #[test]
1394        fn test_document_state_update_cached_versions() {
1395            let deps = vec![create_test_dependency()];
1396            let mut state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1397
1398            let mut cached = HashMap::new();
1399            cached.insert("serde".into(), "1.0.210".into());
1400
1401            state.update_cached_versions(cached);
1402            assert_eq!(state.cached_versions.len(), 1);
1403        }
1404
1405        #[test]
1406        fn test_document_state_parse_result_accessor() {
1407            let deps = vec![create_test_dependency()];
1408            let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1409            assert!(state.parse_result().is_none());
1410        }
1411
1412        #[test]
1413        fn test_document_state_clone() {
1414            let deps = vec![create_test_dependency()];
1415            let state = DocumentState::new(Ecosystem::Cargo, "test content".into(), deps);
1416            let cloned = state.clone();
1417
1418            assert_eq!(cloned.ecosystem, state.ecosystem);
1419            assert_eq!(cloned.content, state.content);
1420            assert_eq!(cloned.dependencies.len(), state.dependencies.len());
1421            assert!(cloned.parse_result.is_none());
1422        }
1423
1424        #[test]
1425        fn test_document_state_debug() {
1426            let deps = vec![create_test_dependency()];
1427            let state = DocumentState::new(Ecosystem::Cargo, "test".into(), deps);
1428            let debug_str = format!("{state:?}");
1429            assert!(debug_str.contains("DocumentState"));
1430        }
1431    }
1432
1433    // =========================================================================
1434    // npm ecosystem tests
1435    // =========================================================================
1436
1437    #[cfg(feature = "npm")]
1438    mod npm_tests {
1439        use super::*;
1440        use deps_npm::{NpmDependency, NpmDependencySection};
1441        use tower_lsp_server::ls_types::{Position, Range};
1442
1443        #[test]
1444        fn test_unified_dependency() {
1445            let npm_dep = UnifiedDependency::Npm(NpmDependency {
1446                name: "express".into(),
1447                name_range: Range::new(Position::new(0, 0), Position::new(0, 7)),
1448                version_req: Some("^4.0.0".into()),
1449                version_range: Some(Range::new(Position::new(0, 11), Position::new(0, 18))),
1450                section: NpmDependencySection::Dependencies,
1451            });
1452
1453            assert_eq!(npm_dep.name(), "express");
1454            assert_eq!(npm_dep.version_req(), Some("^4.0.0"));
1455            assert!(npm_dep.is_registry());
1456        }
1457
1458        #[test]
1459        fn test_unified_version() {
1460            let version = UnifiedVersion::Npm(deps_npm::NpmVersion {
1461                version: "4.18.2".into(),
1462                deprecated: false,
1463            });
1464            assert_eq!(version.version_string(), "4.18.2");
1465            assert!(!version.is_yanked());
1466        }
1467
1468        #[test]
1469        fn test_document_state_new_without_parse_result() {
1470            let content = r#"{"dependencies": {"express": "^4.18.0"}}"#.to_string();
1471            let doc_state = DocumentState::new_without_parse_result("npm", content);
1472
1473            assert_eq!(doc_state.ecosystem_id, "npm");
1474            assert_eq!(doc_state.ecosystem, Ecosystem::Npm);
1475            assert!(doc_state.parse_result.is_none());
1476        }
1477    }
1478
1479    // =========================================================================
1480    // PyPI ecosystem tests
1481    // =========================================================================
1482
1483    #[cfg(feature = "pypi")]
1484    mod pypi_tests {
1485        use super::*;
1486        use deps_pypi::{PypiDependency, PypiDependencySection, PypiDependencySource};
1487        use tower_lsp_server::ls_types::{Position, Range};
1488
1489        #[test]
1490        fn test_unified_dependency() {
1491            let pypi_dep = UnifiedDependency::Pypi(PypiDependency {
1492                name: "requests".into(),
1493                name_range: Range::new(Position::new(0, 0), Position::new(0, 8)),
1494                version_req: Some(">=2.0.0".into()),
1495                version_range: Some(Range::new(Position::new(0, 10), Position::new(0, 18))),
1496                extras: vec![],
1497                extras_range: None,
1498                markers: None,
1499                markers_range: None,
1500                source: PypiDependencySource::Registry,
1501                section: PypiDependencySection::Dependencies,
1502            });
1503
1504            assert_eq!(pypi_dep.name(), "requests");
1505            assert_eq!(pypi_dep.version_req(), Some(">=2.0.0"));
1506            assert!(pypi_dep.is_registry());
1507        }
1508
1509        #[test]
1510        fn test_unified_version() {
1511            let version = UnifiedVersion::Pypi(deps_pypi::PypiVersion {
1512                version: "2.31.0".into(),
1513                yanked: true,
1514            });
1515            assert_eq!(version.version_string(), "2.31.0");
1516            assert!(version.is_yanked());
1517        }
1518
1519        #[test]
1520        fn test_document_state_new_without_parse_result() {
1521            let content = "[project]\ndependencies = [\"requests>=2.0.0\"]\n".to_string();
1522            let doc_state = DocumentState::new_without_parse_result("pypi", content);
1523
1524            assert_eq!(doc_state.ecosystem_id, "pypi");
1525            assert_eq!(doc_state.ecosystem, Ecosystem::Pypi);
1526            assert!(doc_state.parse_result.is_none());
1527        }
1528    }
1529
1530    // =========================================================================
1531    // Go ecosystem tests
1532    // =========================================================================
1533
1534    #[cfg(feature = "go")]
1535    mod go_tests {
1536        use super::*;
1537        use deps_go::{GoDependency, GoDirective, GoVersion};
1538        use tower_lsp_server::ls_types::{Position, Range};
1539
1540        fn create_test_dependency() -> UnifiedDependency {
1541            UnifiedDependency::Go(GoDependency {
1542                module_path: "github.com/gin-gonic/gin".into(),
1543                module_path_range: Range::new(Position::new(0, 0), Position::new(0, 25)),
1544                version: Some("v1.9.1".into()),
1545                version_range: Some(Range::new(Position::new(0, 26), Position::new(0, 32))),
1546                directive: GoDirective::Require,
1547                indirect: false,
1548            })
1549        }
1550
1551        #[test]
1552        fn test_unified_dependency() {
1553            let go_dep = create_test_dependency();
1554            assert_eq!(go_dep.name(), "github.com/gin-gonic/gin");
1555            assert_eq!(go_dep.version_req(), Some("v1.9.1"));
1556            assert!(go_dep.is_registry());
1557        }
1558
1559        #[test]
1560        fn test_unified_dependency_name_range() {
1561            let range = Range::new(Position::new(5, 10), Position::new(5, 35));
1562            let go_dep = UnifiedDependency::Go(GoDependency {
1563                module_path: "github.com/example/pkg".into(),
1564                module_path_range: range,
1565                version: Some("v1.0.0".into()),
1566                version_range: Some(Range::new(Position::new(5, 36), Position::new(5, 42))),
1567                directive: GoDirective::Require,
1568                indirect: false,
1569            });
1570            assert_eq!(go_dep.name_range(), range);
1571        }
1572
1573        #[test]
1574        fn test_unified_dependency_version_range() {
1575            let version_range = Range::new(Position::new(5, 36), Position::new(5, 42));
1576            let go_dep = UnifiedDependency::Go(GoDependency {
1577                module_path: "github.com/example/pkg".into(),
1578                module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1579                version: Some("v1.0.0".into()),
1580                version_range: Some(version_range),
1581                directive: GoDirective::Require,
1582                indirect: false,
1583            });
1584            assert_eq!(go_dep.version_range(), Some(version_range));
1585        }
1586
1587        #[test]
1588        fn test_unified_dependency_no_version() {
1589            let go_dep = UnifiedDependency::Go(GoDependency {
1590                module_path: "github.com/example/pkg".into(),
1591                module_path_range: Range::new(Position::new(5, 10), Position::new(5, 35)),
1592                version: None,
1593                version_range: None,
1594                directive: GoDirective::Require,
1595                indirect: false,
1596            });
1597            assert_eq!(go_dep.version_req(), None);
1598            assert_eq!(go_dep.version_range(), None);
1599        }
1600
1601        #[test]
1602        fn test_unified_version() {
1603            let version = UnifiedVersion::Go(GoVersion {
1604                version: "v1.9.1".into(),
1605                time: Some("2023-07-18T14:30:00Z".into()),
1606                is_pseudo: false,
1607                retracted: false,
1608            });
1609            assert_eq!(version.version_string(), "v1.9.1");
1610            assert!(!version.is_yanked());
1611        }
1612
1613        #[test]
1614        fn test_unified_version_retracted() {
1615            let version = UnifiedVersion::Go(GoVersion {
1616                version: "v1.0.0".into(),
1617                time: None,
1618                is_pseudo: false,
1619                retracted: true,
1620            });
1621            assert_eq!(version.version_string(), "v1.0.0");
1622            assert!(version.is_yanked());
1623        }
1624
1625        #[test]
1626        fn test_unified_version_pseudo() {
1627            let version = UnifiedVersion::Go(GoVersion {
1628                version: "v0.0.0-20191109021931-daa7c04131f5".into(),
1629                time: Some("2019-11-09T02:19:31Z".into()),
1630                is_pseudo: true,
1631                retracted: false,
1632            });
1633            assert_eq!(
1634                version.version_string(),
1635                "v0.0.0-20191109021931-daa7c04131f5"
1636            );
1637            assert!(!version.is_yanked());
1638        }
1639
1640        #[test]
1641        fn test_document_state_new() {
1642            let deps = vec![create_test_dependency()];
1643            let state = DocumentState::new(Ecosystem::Go, "test content".into(), deps);
1644
1645            assert_eq!(state.ecosystem, Ecosystem::Go);
1646            assert_eq!(state.ecosystem_id, "go");
1647            assert_eq!(state.dependencies.len(), 1);
1648        }
1649
1650        #[test]
1651        fn test_document_state_new_without_parse_result() {
1652            let content =
1653                "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1654                    .to_string();
1655            let doc_state = DocumentState::new_without_parse_result("go", content);
1656
1657            assert_eq!(doc_state.ecosystem_id, "go");
1658            assert_eq!(doc_state.ecosystem, Ecosystem::Go);
1659            assert!(doc_state.parse_result.is_none());
1660        }
1661
1662        #[test]
1663        fn test_document_state_new_from_parse_result() {
1664            let state = ServerState::new();
1665            let uri = Uri::from_file_path("/test/go.mod").unwrap();
1666            let ecosystem = state.ecosystem_registry.get("go").unwrap();
1667            let content =
1668                "module example.com/myapp\n\ngo 1.21\n\nrequire github.com/gin-gonic/gin v1.9.1\n"
1669                    .to_string();
1670
1671            let parse_result = tokio::runtime::Runtime::new()
1672                .unwrap()
1673                .block_on(ecosystem.parse_manifest(&content, &uri))
1674                .unwrap();
1675
1676            let doc_state =
1677                DocumentState::new_from_parse_result("go", content.clone(), parse_result);
1678
1679            assert_eq!(doc_state.ecosystem_id, "go");
1680            assert!(doc_state.parse_result.is_some());
1681        }
1682    }
1683}