Skip to content

Commit 1e7cb46

Browse files
authored
feat(lsp): add loading indicators for registry data fetching (#45)
* feat(lsp): add LoadingState tracking to DocumentState Add loading state infrastructure for tracking registry data fetch status. This provides the foundation for loading indicators in editors. Changes: - Add LoadingState enum (Idle, Loading, Loaded, Failed) - Add loading_state and loading_started_at fields to DocumentState - Implement set_loading(), set_loaded(), set_failed() state transitions - Add loading_duration() for elapsed time tracking - Export LoadingState from document module Testing: - 13 unit tests covering all state transitions - Concurrent mutation safety test - Idempotency and edge case tests - Timer reset behavior verification * feat(lsp): add LSP progress notifications for registry fetching Add RegistryProgress struct implementing window/workDoneProgress protocol to show visual loading indicators in editors during version fetching. - Add progress.rs module with RegistryProgress for progress lifecycle - Track client capabilities to check work done progress support - Integrate progress notifications into document open/change handlers - Graceful degradation for clients without progress support - Safe Drop implementation with cleanup task for incomplete progress * feat(lsp): add inlay hints loading fallback for unsupported editors Add LoadingState to deps-core and loading indicator configuration for editors without LSP progress notification support. - Move LoadingState enum to deps-core with comprehensive docs - Add LoadingIndicatorConfig with loading_text validation (max 100 chars) - Update EcosystemConfig with loading_text and show_loading_hints - Update Ecosystem trait generate_inlay_hints with loading_state param - Add loading hint display logic in lsp_helpers - Update all 4 ecosystem implementations - Add comprehensive unit tests for LoadingState and loading hints * test(lsp): add comprehensive loading indicator E2E tests Add 22 end-to-end integration tests for loading indicator feature covering lifecycle, configuration, concurrency, and edge cases. - Add loading state lifecycle tests for cargo ecosystem - Add configuration integration tests (defaults, custom, disabled) - Add loading text validation tests (truncation at 100 chars) - Add concurrent document loading tests - Add timeout scenario and race condition tests - Add Drop cleanup logic verification test - Fix feature flag scope on ecosystem-specific tests * fix(lsp): enable incremental progress updates during version fetching Refactor fetch_latest_versions_parallel() to use stream-based processing with buffer_unordered() instead of join_all(). This allows sending progress updates after each dependency fetch completes. - Add progress parameter to fetch_latest_versions_parallel() - Use AtomicUsize counter to track completed fetches - Call progress.update(count, total) after each fetch - Limit concurrency to 10 parallel requests * fix(lsp): show inlay hints immediately after progress completes Move inlay_hint_refresh() call before generate_diagnostics_internal() so hints appear immediately after loading, without waiting for the slower diagnostics generation which makes 2 network calls per dependency (get_versions + get_latest_matching). Before: Progress → Diagnostics (30s for 100 deps) → Hints After: Progress → Hints (instant) → Diagnostics (background) Performance improvement: 300x faster inlay hints display (30s → 100ms) * test(lsp): add notification capture infrastructure and ordering tests Extend LspClient to capture LSP notifications instead of discarding them, enabling tests that verify notification ordering. - Add CapturedNotification struct with timestamp and sequence number - Implement notification capture in read_response() - Add helper methods: get_notifications(), find_notification(), flush_notifications() - Add workspace.inlayHint.refreshSupport to client capabilities - Add notification_ordering.rs with ordering verification tests
1 parent fbeacb2 commit 1e7cb46

File tree

20 files changed

+2227
-35
lines changed

20 files changed

+2227
-35
lines changed

crates/deps-cargo/src/ecosystem.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ impl Ecosystem for CargoEcosystem {
202202
parse_result: &dyn ParseResultTrait,
203203
cached_versions: &HashMap<String, String>,
204204
resolved_versions: &HashMap<String, String>,
205+
loading_state: deps_core::LoadingState,
205206
config: &EcosystemConfig,
206207
) -> Vec<InlayHint> {
207208
lsp_helpers::generate_inlay_hints(
208209
parse_result,
209210
cached_versions,
210211
resolved_versions,
212+
loading_state,
211213
config,
212214
&self.formatter,
213215
)
@@ -391,6 +393,8 @@ mod tests {
391393
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
392394

393395
let config = EcosystemConfig {
396+
loading_text: "⏳".to_string(),
397+
show_loading_hints: true,
394398
show_up_to_date_hints: true,
395399
up_to_date_text: "✅".to_string(),
396400
needs_update_text: "❌ {}".to_string(),
@@ -401,6 +405,7 @@ mod tests {
401405
&parse_result,
402406
&cached_versions,
403407
&resolved_versions,
408+
deps_core::LoadingState::Loaded,
404409
&config,
405410
));
406411

@@ -424,6 +429,8 @@ mod tests {
424429
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
425430

426431
let config = EcosystemConfig {
432+
loading_text: "⏳".to_string(),
433+
show_loading_hints: true,
427434
show_up_to_date_hints: true,
428435
up_to_date_text: "✅".to_string(),
429436
needs_update_text: "❌ {}".to_string(),
@@ -434,6 +441,7 @@ mod tests {
434441
&parse_result,
435442
&cached_versions,
436443
&resolved_versions,
444+
deps_core::LoadingState::Loaded,
437445
&config,
438446
));
439447

@@ -457,6 +465,8 @@ mod tests {
457465
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
458466

459467
let config = EcosystemConfig {
468+
loading_text: "⏳".to_string(),
469+
show_loading_hints: true,
460470
show_up_to_date_hints: true,
461471
up_to_date_text: "✅".to_string(),
462472
needs_update_text: "❌ {}".to_string(),
@@ -467,6 +477,7 @@ mod tests {
467477
&parse_result,
468478
&cached_versions,
469479
&resolved_versions,
480+
deps_core::LoadingState::Loaded,
470481
&config,
471482
));
472483

@@ -490,6 +501,8 @@ mod tests {
490501
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
491502

492503
let config = EcosystemConfig {
504+
loading_text: "⏳".to_string(),
505+
show_loading_hints: true,
493506
show_up_to_date_hints: false,
494507
up_to_date_text: "✅".to_string(),
495508
needs_update_text: "❌ {}".to_string(),
@@ -500,6 +513,7 @@ mod tests {
500513
&parse_result,
501514
&cached_versions,
502515
&resolved_versions,
516+
deps_core::LoadingState::Loaded,
503517
&config,
504518
));
505519

@@ -522,6 +536,8 @@ mod tests {
522536
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
523537

524538
let config = EcosystemConfig {
539+
loading_text: "⏳".to_string(),
540+
show_loading_hints: true,
525541
show_up_to_date_hints: true,
526542
up_to_date_text: "✅".to_string(),
527543
needs_update_text: "❌ {}".to_string(),
@@ -532,6 +548,7 @@ mod tests {
532548
&parse_result,
533549
&cached_versions,
534550
&resolved_versions,
551+
deps_core::LoadingState::Loaded,
535552
&config,
536553
));
537554

@@ -554,6 +571,8 @@ mod tests {
554571
cached_versions.insert("serde".to_string(), "1.0.214".to_string());
555572

556573
let config = EcosystemConfig {
574+
loading_text: "⏳".to_string(),
575+
show_loading_hints: true,
557576
show_up_to_date_hints: true,
558577
up_to_date_text: "✅".to_string(),
559578
needs_update_text: "❌ {}".to_string(),
@@ -565,6 +584,7 @@ mod tests {
565584
&parse_result,
566585
&cached_versions,
567586
&resolved_versions,
587+
deps_core::LoadingState::Loaded,
568588
&config,
569589
));
570590

@@ -736,4 +756,48 @@ mod tests {
736756
assert!(!results.is_empty());
737757
assert!(results.iter().any(|r| r.label.contains('-')));
738758
}
759+
760+
#[test]
761+
fn test_generate_inlay_hints_loading_state() {
762+
let cache = Arc::new(deps_core::HttpCache::new());
763+
let ecosystem = CargoEcosystem::new(cache);
764+
765+
let parse_result = MockParseResult {
766+
dependencies: vec![mock_dependency("tokio", Some("1.0"), 5, 5)],
767+
};
768+
769+
// Empty caches - simulating loading state
770+
let cached_versions = HashMap::new();
771+
let resolved_versions = HashMap::new();
772+
773+
let config = EcosystemConfig {
774+
loading_text: "⏳".to_string(),
775+
show_loading_hints: true,
776+
show_up_to_date_hints: true,
777+
up_to_date_text: "✅".to_string(),
778+
needs_update_text: "❌ {}".to_string(),
779+
};
780+
781+
let hints = tokio_test::block_on(ecosystem.generate_inlay_hints(
782+
&parse_result,
783+
&cached_versions,
784+
&resolved_versions,
785+
deps_core::LoadingState::Loading,
786+
&config,
787+
));
788+
789+
assert_eq!(hints.len(), 1);
790+
match &hints[0].label {
791+
InlayHintLabel::String(s) => assert_eq!(s, "⏳", "Expected loading indicator"),
792+
_ => panic!("Expected String label"),
793+
}
794+
795+
if let Some(tower_lsp_server::ls_types::InlayHintTooltip::String(tooltip)) =
796+
&hints[0].tooltip
797+
{
798+
assert_eq!(tooltip, "Fetching latest version...");
799+
} else {
800+
panic!("Expected tooltip for loading state");
801+
}
802+
}
739803
}

crates/deps-core/src/ecosystem.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ pub struct EcosystemConfig {
6262
pub up_to_date_text: String,
6363
/// Text to display for dependencies needing updates (use {} for version placeholder)
6464
pub needs_update_text: String,
65+
/// Text to display while loading registry data
66+
pub loading_text: String,
67+
/// Whether to show loading hints in inlay hints
68+
pub show_loading_hints: bool,
6569
}
6670

6771
impl Default for EcosystemConfig {
@@ -70,6 +74,8 @@ impl Default for EcosystemConfig {
7074
show_up_to_date_hints: true,
7175
up_to_date_text: "✅".to_string(),
7276
needs_update_text: "❌ {}".to_string(),
77+
loading_text: "⏳".to_string(),
78+
show_loading_hints: true,
7379
}
7480
}
7581
}
@@ -130,9 +136,10 @@ impl Default for EcosystemConfig {
130136
/// parse_result: &dyn ParseResult,
131137
/// cached_versions: &std::collections::HashMap<String, String>,
132138
/// resolved_versions: &std::collections::HashMap<String, String>,
139+
/// loading_state: deps_core::LoadingState,
133140
/// config: &EcosystemConfig,
134141
/// ) -> Vec<InlayHint> {
135-
/// let _ = resolved_versions; // Use resolved versions for lock file support
142+
/// let _ = (resolved_versions, loading_state); // Use resolved versions for lock file support
136143
/// vec![]
137144
/// }
138145
///
@@ -249,12 +256,14 @@ pub trait Ecosystem: Send + Sync {
249256
/// * `parse_result` - Parsed dependencies from manifest
250257
/// * `cached_versions` - Pre-fetched version information (name -> latest version from registry)
251258
/// * `resolved_versions` - Resolved versions from lock file (name -> locked version)
259+
/// * `loading_state` - Current loading state for registry data
252260
/// * `config` - User configuration for hint display
253261
async fn generate_inlay_hints(
254262
&self,
255263
parse_result: &dyn ParseResult,
256264
cached_versions: &std::collections::HashMap<String, String>,
257265
resolved_versions: &std::collections::HashMap<String, String>,
266+
loading_state: crate::LoadingState,
258267
config: &EcosystemConfig,
259268
) -> Vec<InlayHint>;
260269

@@ -350,6 +359,8 @@ mod tests {
350359
show_up_to_date_hints: false,
351360
up_to_date_text: "OK".to_string(),
352361
needs_update_text: "Update to {}".to_string(),
362+
loading_text: "Loading...".to_string(),
363+
show_loading_hints: false,
353364
};
354365
assert!(!config.show_up_to_date_hints);
355366
assert_eq!(config.up_to_date_text, "OK");

crates/deps-core/src/ecosystem_registry.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ mod tests {
319319
_parse_result: &dyn ParseResult,
320320
_cached_versions: &std::collections::HashMap<String, String>,
321321
_resolved_versions: &std::collections::HashMap<String, String>,
322+
_loading_state: crate::LoadingState,
322323
_config: &EcosystemConfig,
323324
) -> Vec<InlayHint> {
324325
vec![]

crates/deps-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ pub use lsp_helpers::{
116116
generate_diagnostics as lsp_generate_diagnostics, generate_hover as lsp_generate_hover,
117117
generate_inlay_hints as lsp_generate_inlay_hints, is_same_major_minor, ranges_overlap,
118118
};
119-
pub use parser::{DependencyInfo, DependencySource, ManifestParser, ParseResultInfo};
119+
pub use parser::{DependencyInfo, DependencySource, LoadingState, ManifestParser, ParseResultInfo};
120120
pub use registry::{
121121
Metadata, PackageMetadata, PackageRegistry, Registry, Version, VersionInfo, find_latest_stable,
122122
};

0 commit comments

Comments
 (0)