Skip to main content

deps_dart/
ecosystem.rs

1//! Dart ecosystem implementation for deps-lsp.
2
3use std::any::Any;
4use std::sync::Arc;
5use tower_lsp_server::ls_types::{CompletionItem, Position, Range, Uri};
6
7use deps_core::{
8    Ecosystem, ParseResult as ParseResultTrait, Registry, Result, completion::Completions,
9    lsp_helpers::EcosystemFormatter,
10};
11
12use crate::formatter::DartFormatter;
13use crate::registry::PubDevRegistry;
14
15pub struct DartEcosystem {
16    registry: Arc<PubDevRegistry>,
17    formatter: DartFormatter,
18}
19
20impl DartEcosystem {
21    pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
22        Self {
23            registry: Arc::new(PubDevRegistry::new(cache)),
24            formatter: DartFormatter,
25        }
26    }
27
28    async fn complete_package_names(&self, prefix: &str, range: Range) -> Vec<CompletionItem> {
29        deps_core::completion::complete_package_names_generic(
30            self.registry.as_ref(),
31            prefix,
32            20,
33            range,
34        )
35        .await
36    }
37
38    async fn complete_versions(
39        &self,
40        package_name: &deps_core::PackageName,
41        prefix: &str,
42        freshness: deps_core::FreshnessSettings,
43    ) -> Vec<CompletionItem> {
44        deps_core::completion::complete_versions_generic(
45            self.registry.as_ref(),
46            package_name,
47            prefix,
48            &['^', '>', '<', '='],
49            freshness,
50        )
51        .await
52    }
53}
54
55impl deps_core::ecosystem::private::Sealed for DartEcosystem {}
56
57impl Ecosystem for DartEcosystem {
58    fn id(&self) -> &'static str {
59        "dart"
60    }
61
62    fn display_name(&self) -> &'static str {
63        "Dart (Pub)"
64    }
65
66    fn manifest_filenames(&self) -> &[&'static str] {
67        &["pubspec.yaml"]
68    }
69
70    fn lockfile_filenames(&self) -> &[&'static str] {
71        &["pubspec.lock"]
72    }
73
74    fn parse_manifest<'a>(
75        &'a self,
76        content: &'a str,
77        uri: &'a Uri,
78    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
79        Box::pin(async move {
80            let result = crate::parser::parse_pubspec_yaml(content, uri)?;
81            Ok(Box::new(result) as Box<dyn ParseResultTrait>)
82        })
83    }
84
85    fn registry(&self) -> Arc<dyn Registry> {
86        self.registry.clone() as Arc<dyn Registry>
87    }
88
89    fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
90        Some(Arc::new(crate::lockfile::PubspecLockParser))
91    }
92
93    fn formatter(&self) -> &dyn EcosystemFormatter {
94        &self.formatter
95    }
96
97    fn generate_completions<'a>(
98        &'a self,
99        parse_result: &'a dyn ParseResultTrait,
100        position: Position,
101        content: &'a str,
102        freshness: deps_core::FreshnessSettings,
103    ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
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, range } => {
111                    self.complete_package_names(&prefix, range).await
112                }
113                CompletionContext::Version {
114                    package_name,
115                    prefix,
116                } => {
117                    self.complete_versions(&package_name, &prefix, freshness)
118                        .await
119                }
120                CompletionContext::Feature { .. } | CompletionContext::None => vec![],
121            }
122            .into()
123        })
124    }
125
126    fn as_any(&self) -> &dyn Any {
127        self
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_ecosystem_id() {
137        let cache = Arc::new(deps_core::HttpCache::new());
138        let eco = DartEcosystem::new(cache);
139        assert_eq!(eco.id(), "dart");
140    }
141
142    #[test]
143    fn test_ecosystem_display_name() {
144        let cache = Arc::new(deps_core::HttpCache::new());
145        let eco = DartEcosystem::new(cache);
146        assert_eq!(eco.display_name(), "Dart (Pub)");
147    }
148
149    #[test]
150    fn test_ecosystem_manifest_filenames() {
151        let cache = Arc::new(deps_core::HttpCache::new());
152        let eco = DartEcosystem::new(cache);
153        assert_eq!(eco.manifest_filenames(), &["pubspec.yaml"]);
154    }
155
156    #[test]
157    fn test_ecosystem_lockfile_filenames() {
158        let cache = Arc::new(deps_core::HttpCache::new());
159        let eco = DartEcosystem::new(cache);
160        assert_eq!(eco.lockfile_filenames(), &["pubspec.lock"]);
161    }
162
163    #[test]
164    fn test_as_any() {
165        let cache = Arc::new(deps_core::HttpCache::new());
166        let eco = DartEcosystem::new(cache);
167        assert!(eco.as_any().is::<DartEcosystem>());
168    }
169
170    #[tokio::test]
171    async fn test_package_name_completion_context_has_real_range() {
172        // Regression test for #232: the textEdit range for a package-name completion
173        // must be the real name token span, not the (0,0)-(0,0) placeholder.
174        let cache = Arc::new(deps_core::HttpCache::new());
175        let eco = DartEcosystem::new(cache);
176        let content = "name: my_app\ndependencies:\n  http: ^1.0.0\n";
177        let uri = deps_core::test_util::test_uri("/test/pubspec.yaml");
178
179        let parse_result = eco.parse_manifest(content, &uri).await.unwrap();
180        let position = Position::new(2, 4); // cursor after "ht" in "http"
181
182        let context = deps_core::completion::detect_completion_context(
183            parse_result.as_ref(),
184            position,
185            content,
186        );
187
188        match context {
189            deps_core::completion::CompletionContext::PackageName { prefix, range } => {
190                assert_eq!(prefix, "ht");
191                assert_ne!(range, Range::default());
192                assert_eq!(range, Range::new(Position::new(2, 2), Position::new(2, 6)));
193            }
194            other => panic!("Expected PackageName context, got {other:?}"),
195        }
196    }
197
198    #[tokio::test]
199    async fn test_complete_package_names_min_prefix() {
200        let cache = Arc::new(deps_core::HttpCache::new());
201        let eco = DartEcosystem::new(cache);
202        assert!(
203            eco.complete_package_names("h", Range::default())
204                .await
205                .is_empty()
206        );
207        assert!(
208            eco.complete_package_names("", Range::default())
209                .await
210                .is_empty()
211        );
212    }
213
214    #[tokio::test]
215    async fn test_complete_package_names_max_length() {
216        let cache = Arc::new(deps_core::HttpCache::new());
217        let eco = DartEcosystem::new(cache);
218        let long = "a".repeat(201);
219        assert!(
220            eco.complete_package_names(&long, Range::default())
221                .await
222                .is_empty()
223        );
224    }
225
226    #[tokio::test]
227    async fn test_lockfile_provider() {
228        let cache = Arc::new(deps_core::HttpCache::new());
229        let eco = DartEcosystem::new(cache);
230        assert!(eco.lockfile_provider().is_some());
231    }
232
233    #[tokio::test]
234    async fn test_parse_manifest() {
235        let cache = Arc::new(deps_core::HttpCache::new());
236        let eco = DartEcosystem::new(cache);
237
238        let yaml = "name: app\ndependencies:\n  http: ^1.0.0\n";
239        #[cfg(windows)]
240        let path = "C:/test/pubspec.yaml";
241        #[cfg(not(windows))]
242        let path = "/test/pubspec.yaml";
243        let uri = Uri::from_file_path(path).unwrap();
244
245        let result = eco.parse_manifest(yaml, &uri).await.unwrap();
246        assert_eq!(result.dependencies().len(), 1);
247    }
248}