Skip to main content

deps_deno/
ecosystem.rs

1//! Deno ecosystem implementation for deps-lsp (D1).
2//!
3//! Provides LSP functionality for `deno.json`/`deno.jsonc` files: dependency parsing with
4//! position tracking, `jsr:`/`npm:` version lookups via [`crate::registry::DenoRegistry`],
5//! inlay hints, hover, code actions, and diagnostics — all via `deps-core`'s generic
6//! handlers, with no Deno-specific handler code (FR-010).
7
8use std::any::Any;
9use std::sync::Arc;
10use tower_lsp_server::ls_types::{CompletionItem, Position, Range, Uri};
11
12use deps_core::{
13    Ecosystem, ParseResult as ParseResultTrait, Registry, Result, completion::Completions,
14    lsp_helpers::EcosystemFormatter,
15};
16
17use crate::formatter::DenoFormatter;
18use crate::registry::DenoRegistry;
19use deps_npm::NpmRegistry;
20
21/// Deno ecosystem implementation.
22///
23/// Provides LSP functionality for `deno.json`/`deno.jsonc` files, including:
24/// - Dependency parsing with position tracking (`imports` map only, D8)
25/// - Version information from the JSR and npm registries, dispatched by
26///   [`DenoRegistry`] (D3)
27/// - Inlay hints, hover, code actions, and diagnostics via the shared `deps-core`
28///   handlers
29///
30/// No lock file support in the MVP (D9): `deno.lock` resolved-version parsing is a
31/// follow-up increment.
32pub struct DenoEcosystem {
33    registry: Arc<DenoRegistry>,
34    formatter: DenoFormatter,
35}
36
37impl DenoEcosystem {
38    /// Creates a new Deno ecosystem with the given HTTP cache.
39    ///
40    /// The same cache backs both halves of the registry facade (M1), deduping plain
41    /// cached GETs between `package.json` and `deno.json` for the same npm package. This
42    /// does not extend to npm's separate freshness-path packument fetch, which bypasses
43    /// `HttpCache` and is memoized per `NpmRegistry` instance — see
44    /// [`DenoRegistry::new`](crate::registry::DenoRegistry::new)'s docs for the full
45    /// caveat (N4). Use [`Self::with_npm`] to avoid it.
46    pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
47        Self {
48            registry: Arc::new(DenoRegistry::new(cache)),
49            formatter: DenoFormatter,
50        }
51    }
52
53    /// Creates a new Deno ecosystem sharing an existing [`NpmRegistry`] instance for its
54    /// `npm:`-scheme half, instead of building a private one (N4/#312).
55    ///
56    /// `deps-lsp`'s ecosystem registration uses this when both the `npm` and `deno`
57    /// features are enabled, so a package appearing in both `package.json` and
58    /// `deno.json` shares one freshness-path publish-time cache. See
59    /// [`DenoRegistry::with_npm`](crate::registry::DenoRegistry::with_npm) for what this
60    /// dedupes.
61    #[must_use]
62    pub fn with_npm(cache: Arc<deps_core::HttpCache>, npm: NpmRegistry) -> Self {
63        Self {
64            registry: Arc::new(DenoRegistry::with_npm(cache, npm)),
65            formatter: DenoFormatter,
66        }
67    }
68
69    /// Completes package names by searching whichever registry the typed scheme prefix
70    /// (`jsr:`/`npm:`) selects.
71    async fn complete_package_names(&self, prefix: &str, range: Range) -> Vec<CompletionItem> {
72        deps_core::completion::complete_package_names_generic(
73            self.registry.as_ref(),
74            prefix,
75            20,
76            range,
77        )
78        .await
79    }
80
81    async fn complete_versions(
82        &self,
83        package_name: &deps_core::PackageName,
84        prefix: &str,
85        freshness: deps_core::FreshnessSettings,
86    ) -> Vec<CompletionItem> {
87        deps_core::completion::complete_versions_generic(
88            self.registry.as_ref(),
89            package_name,
90            prefix,
91            &['^', '~', '=', '<', '>', '*'],
92            freshness,
93        )
94        .await
95    }
96}
97
98impl deps_core::ecosystem::private::Sealed for DenoEcosystem {}
99
100impl Ecosystem for DenoEcosystem {
101    fn id(&self) -> &'static str {
102        "deno"
103    }
104
105    fn display_name(&self) -> &'static str {
106        "Deno (JSR/npm)"
107    }
108
109    fn manifest_filenames(&self) -> &[&'static str] {
110        &["deno.json", "deno.jsonc"]
111    }
112
113    fn parse_manifest<'a>(
114        &'a self,
115        content: &'a str,
116        uri: &'a Uri,
117    ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
118        Box::pin(async move {
119            let result = crate::parser::parse_deno_json(content, uri)?;
120            Ok(Box::new(result) as Box<dyn ParseResultTrait>)
121        })
122    }
123
124    fn registry(&self) -> Arc<dyn Registry> {
125        self.registry.clone() as Arc<dyn Registry>
126    }
127
128    fn formatter(&self) -> &dyn EcosystemFormatter {
129        &self.formatter
130    }
131
132    fn generate_completions<'a>(
133        &'a self,
134        parse_result: &'a dyn ParseResultTrait,
135        position: Position,
136        content: &'a str,
137        freshness: deps_core::FreshnessSettings,
138    ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
139        Box::pin(async move {
140            use deps_core::completion::{CompletionContext, detect_completion_context};
141
142            let context = detect_completion_context(parse_result, position, content);
143
144            match context {
145                CompletionContext::PackageName { prefix, range } => {
146                    self.complete_package_names(&prefix, range).await
147                }
148                CompletionContext::Version {
149                    package_name,
150                    prefix,
151                } => {
152                    self.complete_versions(&package_name, &prefix, freshness)
153                        .await
154                }
155                CompletionContext::Feature { .. } => vec![],
156                CompletionContext::None => vec![],
157            }
158            .into()
159        })
160    }
161
162    fn as_any(&self) -> &dyn Any {
163        self
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_ecosystem_id() {
173        let cache = Arc::new(deps_core::HttpCache::new());
174        let ecosystem = DenoEcosystem::new(cache);
175        assert_eq!(ecosystem.id(), "deno");
176    }
177
178    #[test]
179    fn test_ecosystem_display_name() {
180        let cache = Arc::new(deps_core::HttpCache::new());
181        let ecosystem = DenoEcosystem::new(cache);
182        assert_eq!(ecosystem.display_name(), "Deno (JSR/npm)");
183    }
184
185    #[test]
186    fn test_ecosystem_manifest_filenames() {
187        let cache = Arc::new(deps_core::HttpCache::new());
188        let ecosystem = DenoEcosystem::new(cache);
189        assert_eq!(ecosystem.manifest_filenames(), &["deno.json", "deno.jsonc"]);
190    }
191
192    #[test]
193    fn test_ecosystem_no_lockfile_support() {
194        let cache = Arc::new(deps_core::HttpCache::new());
195        let ecosystem = DenoEcosystem::new(cache);
196        assert!(ecosystem.lockfile_filenames().is_empty());
197        assert!(ecosystem.lockfile_provider().is_none());
198    }
199
200    #[test]
201    fn test_as_any() {
202        let cache = Arc::new(deps_core::HttpCache::new());
203        let ecosystem = DenoEcosystem::new(cache);
204        assert!(ecosystem.as_any().is::<DenoEcosystem>());
205    }
206
207    #[tokio::test]
208    async fn test_parse_manifest_valid_json() {
209        let cache = Arc::new(deps_core::HttpCache::new());
210        let ecosystem = DenoEcosystem::new(cache);
211        let uri = deps_core::test_util::test_uri("/test/deno.json");
212
213        let content = r#"{"imports": {"@std/fs": "jsr:@std/fs@^1.0"}}"#;
214
215        let result = ecosystem.parse_manifest(content, &uri).await;
216        assert!(result.is_ok());
217        assert!(!result.unwrap().dependencies().is_empty());
218    }
219
220    #[tokio::test]
221    async fn test_parse_manifest_invalid_json() {
222        let cache = Arc::new(deps_core::HttpCache::new());
223        let ecosystem = DenoEcosystem::new(cache);
224        let uri = deps_core::test_util::test_uri("/test/deno.json");
225
226        let result = ecosystem.parse_manifest("{ not valid !!", &uri).await;
227        assert!(result.is_err());
228    }
229
230    #[tokio::test]
231    async fn test_registry_returns_arc() {
232        let cache = Arc::new(deps_core::HttpCache::new());
233        let ecosystem = DenoEcosystem::new(cache);
234        let registry = ecosystem.registry();
235        assert!(Arc::strong_count(&registry) >= 1);
236    }
237
238    #[tokio::test]
239    async fn test_generate_completions_no_context() {
240        let cache = Arc::new(deps_core::HttpCache::new());
241        let ecosystem = DenoEcosystem::new(cache);
242        let uri = deps_core::test_util::test_uri("/test/deno.json");
243
244        let content = r#"{"name": "test"}"#;
245        let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
246        let position = Position::new(0, 0);
247
248        let completions = ecosystem
249            .generate_completions(
250                parse_result.as_ref(),
251                position,
252                content,
253                deps_core::FreshnessSettings::default(),
254            )
255            .await;
256
257        assert!(completions.items.is_empty());
258    }
259
260    #[tokio::test]
261    async fn test_complete_package_names_minimum_prefix() {
262        let cache = Arc::new(deps_core::HttpCache::new());
263        let ecosystem = DenoEcosystem::new(cache);
264
265        let results = ecosystem
266            .complete_package_names("j", Range::default())
267            .await;
268        assert!(results.is_empty());
269    }
270
271    #[tokio::test]
272    async fn test_complete_versions_unknown_package() {
273        let cache = Arc::new(deps_core::HttpCache::new());
274        let ecosystem = DenoEcosystem::new(cache);
275
276        let results = ecosystem
277            .complete_versions(
278                &deps_core::PackageName::new("jsr:@this-scope/does-not-exist-12345"),
279                "1.0",
280                deps_core::FreshnessSettings::default(),
281            )
282            .await;
283        assert!(results.is_empty());
284    }
285}