|
| 1 | +//! Swift ecosystem implementation for deps-lsp. |
| 2 | +
|
| 3 | +use std::any::Any; |
| 4 | +use std::sync::Arc; |
| 5 | +use tower_lsp_server::ls_types::{CompletionItem, Position, Uri}; |
| 6 | + |
| 7 | +use deps_core::{ |
| 8 | + Ecosystem, ParseResult as ParseResultTrait, Registry, Result, lsp_helpers::EcosystemFormatter, |
| 9 | +}; |
| 10 | + |
| 11 | +use crate::formatter::SwiftFormatter; |
| 12 | +use crate::lockfile::SwiftLockParser; |
| 13 | +use crate::registry::SwiftRegistry; |
| 14 | + |
| 15 | +/// Swift/SPM ecosystem implementation. |
| 16 | +/// |
| 17 | +/// Provides LSP functionality for Package.swift files, including: |
| 18 | +/// - Dependency parsing with position tracking |
| 19 | +/// - Version information from GitHub tags |
| 20 | +/// - Inlay hints for latest versions |
| 21 | +/// - Hover tooltips with package metadata |
| 22 | +/// - Code actions for version updates |
| 23 | +/// - Diagnostics for unknown packages |
| 24 | +pub struct SwiftEcosystem { |
| 25 | + registry: Arc<SwiftRegistry>, |
| 26 | + formatter: SwiftFormatter, |
| 27 | + lockfile_provider: Arc<SwiftLockParser>, |
| 28 | +} |
| 29 | + |
| 30 | +impl SwiftEcosystem { |
| 31 | + /// Creates a new Swift ecosystem with the given HTTP cache. |
| 32 | + pub fn new(cache: Arc<deps_core::HttpCache>) -> Self { |
| 33 | + Self { |
| 34 | + registry: Arc::new(SwiftRegistry::new(cache)), |
| 35 | + formatter: SwiftFormatter, |
| 36 | + lockfile_provider: Arc::new(SwiftLockParser), |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + async fn complete_package_names(&self, prefix: &str) -> Vec<CompletionItem> { |
| 41 | + deps_core::completion::complete_package_names_generic(self.registry.as_ref(), prefix, 20) |
| 42 | + .await |
| 43 | + } |
| 44 | + |
| 45 | + async fn complete_versions(&self, package_name: &str, prefix: &str) -> Vec<CompletionItem> { |
| 46 | + deps_core::completion::complete_versions_generic( |
| 47 | + self.registry.as_ref(), |
| 48 | + package_name, |
| 49 | + prefix, |
| 50 | + &[], |
| 51 | + ) |
| 52 | + .await |
| 53 | + } |
| 54 | +} |
| 55 | + |
| 56 | +impl deps_core::ecosystem::private::Sealed for SwiftEcosystem {} |
| 57 | + |
| 58 | +impl Ecosystem for SwiftEcosystem { |
| 59 | + fn id(&self) -> &'static str { |
| 60 | + "swift" |
| 61 | + } |
| 62 | + |
| 63 | + fn display_name(&self) -> &'static str { |
| 64 | + "Swift (SPM)" |
| 65 | + } |
| 66 | + |
| 67 | + fn manifest_filenames(&self) -> &[&'static str] { |
| 68 | + &["Package.swift"] |
| 69 | + } |
| 70 | + |
| 71 | + fn lockfile_filenames(&self) -> &[&'static str] { |
| 72 | + &["Package.resolved"] |
| 73 | + } |
| 74 | + |
| 75 | + fn parse_manifest<'a>( |
| 76 | + &'a self, |
| 77 | + content: &'a str, |
| 78 | + uri: &'a Uri, |
| 79 | + ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> { |
| 80 | + Box::pin(async move { |
| 81 | + let result = crate::parser::parse_package_swift(content, uri)?; |
| 82 | + Ok(Box::new(result) as Box<dyn ParseResultTrait>) |
| 83 | + }) |
| 84 | + } |
| 85 | + |
| 86 | + fn registry(&self) -> Arc<dyn Registry> { |
| 87 | + self.registry.clone() as Arc<dyn Registry> |
| 88 | + } |
| 89 | + |
| 90 | + fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> { |
| 91 | + Some(self.lockfile_provider.clone() as Arc<dyn deps_core::lockfile::LockFileProvider>) |
| 92 | + } |
| 93 | + |
| 94 | + fn formatter(&self) -> &dyn EcosystemFormatter { |
| 95 | + &self.formatter |
| 96 | + } |
| 97 | + |
| 98 | + fn generate_completions<'a>( |
| 99 | + &'a self, |
| 100 | + parse_result: &'a dyn ParseResultTrait, |
| 101 | + position: Position, |
| 102 | + content: &'a str, |
| 103 | + ) -> deps_core::ecosystem::BoxFuture<'a, Vec<CompletionItem>> { |
| 104 | + Box::pin(async move { |
| 105 | + use deps_core::completion::{CompletionContext, detect_completion_context}; |
| 106 | + |
| 107 | + let context = detect_completion_context(parse_result, position, content); |
| 108 | + |
| 109 | + match context { |
| 110 | + CompletionContext::PackageName { prefix } => { |
| 111 | + // Strip "https://github.com/" prefix for search query |
| 112 | + let query = prefix |
| 113 | + .strip_prefix("https://github.com/") |
| 114 | + .or_else(|| prefix.strip_prefix("https://github.com")) |
| 115 | + .unwrap_or(&prefix); |
| 116 | + if query.len() < 2 { |
| 117 | + return vec![]; |
| 118 | + } |
| 119 | + self.complete_package_names(query).await |
| 120 | + } |
| 121 | + CompletionContext::Version { |
| 122 | + package_name, |
| 123 | + prefix, |
| 124 | + } => self.complete_versions(&package_name, &prefix).await, |
| 125 | + CompletionContext::Feature { .. } => vec![], |
| 126 | + CompletionContext::None => vec![], |
| 127 | + } |
| 128 | + }) |
| 129 | + } |
| 130 | + |
| 131 | + fn as_any(&self) -> &dyn Any { |
| 132 | + self |
| 133 | + } |
| 134 | +} |
| 135 | + |
| 136 | +#[cfg(test)] |
| 137 | +mod tests { |
| 138 | + use super::*; |
| 139 | + |
| 140 | + #[test] |
| 141 | + fn test_ecosystem_id() { |
| 142 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 143 | + let eco = SwiftEcosystem::new(cache); |
| 144 | + assert_eq!(eco.id(), "swift"); |
| 145 | + } |
| 146 | + |
| 147 | + #[test] |
| 148 | + fn test_ecosystem_display_name() { |
| 149 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 150 | + let eco = SwiftEcosystem::new(cache); |
| 151 | + assert_eq!(eco.display_name(), "Swift (SPM)"); |
| 152 | + } |
| 153 | + |
| 154 | + #[test] |
| 155 | + fn test_manifest_filenames() { |
| 156 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 157 | + let eco = SwiftEcosystem::new(cache); |
| 158 | + assert_eq!(eco.manifest_filenames(), &["Package.swift"]); |
| 159 | + assert_eq!(eco.lockfile_filenames(), &["Package.resolved"]); |
| 160 | + } |
| 161 | + |
| 162 | + #[test] |
| 163 | + fn test_as_any() { |
| 164 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 165 | + let eco = SwiftEcosystem::new(cache); |
| 166 | + assert!(eco.as_any().is::<SwiftEcosystem>()); |
| 167 | + } |
| 168 | + |
| 169 | + #[test] |
| 170 | + fn test_lockfile_provider_some() { |
| 171 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 172 | + let eco = SwiftEcosystem::new(cache); |
| 173 | + assert!(eco.lockfile_provider().is_some()); |
| 174 | + } |
| 175 | + |
| 176 | + #[tokio::test] |
| 177 | + async fn test_parse_manifest_valid() { |
| 178 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 179 | + let eco = SwiftEcosystem::new(cache); |
| 180 | + let uri = Uri::from_file_path("/test/Package.swift").unwrap(); |
| 181 | + let content = r#".package(url: "https://github.com/apple/swift-nio.git", from: "2.40.0")"#; |
| 182 | + let result = eco.parse_manifest(content, &uri).await; |
| 183 | + assert!(result.is_ok()); |
| 184 | + assert_eq!(result.unwrap().dependencies().len(), 1); |
| 185 | + } |
| 186 | + |
| 187 | + #[tokio::test] |
| 188 | + async fn test_parse_manifest_empty() { |
| 189 | + let cache = Arc::new(deps_core::HttpCache::new()); |
| 190 | + let eco = SwiftEcosystem::new(cache); |
| 191 | + let uri = Uri::from_file_path("/test/Package.swift").unwrap(); |
| 192 | + let result = eco.parse_manifest("// empty file", &uri).await; |
| 193 | + assert!(result.is_ok()); |
| 194 | + assert!(result.unwrap().dependencies().is_empty()); |
| 195 | + } |
| 196 | +} |
0 commit comments