1use std::any::Any;
7use std::sync::Arc;
8use tower_lsp_server::ls_types::{CompletionItem, Position, Range, Uri};
9
10use deps_core::{
11 Ecosystem, ParseResult as ParseResultTrait, Registry, Result, completion::Completions,
12 lsp_helpers::EcosystemFormatter,
13};
14
15use crate::formatter::ComposerFormatter;
16use crate::registry::PackagistRegistry;
17
18pub struct ComposerEcosystem {
28 registry: Arc<PackagistRegistry>,
29 formatter: ComposerFormatter,
30}
31
32impl ComposerEcosystem {
33 pub fn new(cache: Arc<deps_core::HttpCache>) -> Self {
35 Self {
36 registry: Arc::new(PackagistRegistry::new(cache)),
37 formatter: ComposerFormatter,
38 }
39 }
40
41 async fn complete_package_names(&self, prefix: &str, range: Range) -> Vec<CompletionItem> {
42 deps_core::completion::complete_package_names_generic(
43 self.registry.as_ref(),
44 prefix,
45 20,
46 range,
47 )
48 .await
49 }
50
51 async fn complete_versions(
52 &self,
53 package_name: &deps_core::PackageName,
54 prefix: &str,
55 freshness: deps_core::FreshnessSettings,
56 ) -> Vec<CompletionItem> {
57 deps_core::completion::complete_versions_generic(
58 self.registry.as_ref(),
59 package_name,
60 prefix,
61 &['^', '~', '=', '<', '>', '*'],
62 freshness,
63 )
64 .await
65 }
66}
67
68impl deps_core::ecosystem::private::Sealed for ComposerEcosystem {}
69
70impl Ecosystem for ComposerEcosystem {
71 fn id(&self) -> &'static str {
72 "composer"
73 }
74
75 fn display_name(&self) -> &'static str {
76 "Composer (PHP)"
77 }
78
79 fn manifest_filenames(&self) -> &[&'static str] {
80 &["composer.json"]
81 }
82
83 fn lockfile_filenames(&self) -> &[&'static str] {
84 &["composer.lock"]
85 }
86
87 fn parse_manifest<'a>(
88 &'a self,
89 content: &'a str,
90 uri: &'a Uri,
91 ) -> deps_core::ecosystem::BoxFuture<'a, Result<Box<dyn ParseResultTrait>>> {
92 Box::pin(async move {
93 let result = crate::parser::parse_composer_json(content, uri)?;
94 Ok(Box::new(result) as Box<dyn ParseResultTrait>)
95 })
96 }
97
98 fn registry(&self) -> Arc<dyn Registry> {
99 self.registry.clone() as Arc<dyn Registry>
100 }
101
102 fn lockfile_provider(&self) -> Option<Arc<dyn deps_core::lockfile::LockFileProvider>> {
103 Some(Arc::new(crate::lockfile::ComposerLockParser))
104 }
105
106 fn formatter(&self) -> &dyn EcosystemFormatter {
107 &self.formatter
108 }
109
110 fn generate_completions<'a>(
111 &'a self,
112 parse_result: &'a dyn ParseResultTrait,
113 position: Position,
114 content: &'a str,
115 freshness: deps_core::FreshnessSettings,
116 ) -> deps_core::ecosystem::BoxFuture<'a, Completions> {
117 Box::pin(async move {
118 use deps_core::completion::{CompletionContext, detect_completion_context};
119
120 let context = detect_completion_context(parse_result, position, content);
121
122 match context {
123 CompletionContext::PackageName { prefix, range } => {
124 self.complete_package_names(&prefix, range).await
125 }
126 CompletionContext::Version {
127 package_name,
128 prefix,
129 } => {
130 self.complete_versions(&package_name, &prefix, freshness)
131 .await
132 }
133 CompletionContext::Feature { .. } => vec![],
134 CompletionContext::None => vec![],
135 }
136 .into()
137 })
138 }
139
140 fn as_any(&self) -> &dyn Any {
141 self
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use deps_core::{EcosystemConfig, VersionData};
149 use std::collections::HashMap;
150
151 #[test]
152 fn test_ecosystem_id() {
153 let cache = Arc::new(deps_core::HttpCache::new());
154 let ecosystem = ComposerEcosystem::new(cache);
155 assert_eq!(ecosystem.id(), "composer");
156 }
157
158 #[test]
159 fn test_ecosystem_manifest_filenames() {
160 let cache = Arc::new(deps_core::HttpCache::new());
161 let ecosystem = ComposerEcosystem::new(cache);
162 assert_eq!(ecosystem.manifest_filenames(), &["composer.json"]);
163 }
164
165 #[test]
166 fn test_ecosystem_lockfile_filenames() {
167 let cache = Arc::new(deps_core::HttpCache::new());
168 let ecosystem = ComposerEcosystem::new(cache);
169 assert_eq!(ecosystem.lockfile_filenames(), &["composer.lock"]);
170 }
171
172 #[test]
173 fn test_lockfile_provider_returns_some() {
174 let cache = Arc::new(deps_core::HttpCache::new());
175 let ecosystem = ComposerEcosystem::new(cache);
176 assert!(ecosystem.lockfile_provider().is_some());
177 }
178
179 #[tokio::test]
180 async fn test_parse_manifest_valid() {
181 let cache = Arc::new(deps_core::HttpCache::new());
182 let ecosystem = ComposerEcosystem::new(cache);
183 let uri = deps_core::test_util::test_uri("/test/composer.json");
184
185 let content = r#"{"require": {"symfony/console": "^6.0"}}"#;
186 let result = ecosystem.parse_manifest(content, &uri).await;
187 assert!(result.is_ok());
188
189 let parse_result = result.unwrap();
190 assert_eq!(parse_result.dependencies().len(), 1);
191 }
192
193 #[tokio::test]
194 async fn test_parse_manifest_invalid() {
195 let cache = Arc::new(deps_core::HttpCache::new());
196 let ecosystem = ComposerEcosystem::new(cache);
197 let uri = deps_core::test_util::test_uri("/test/composer.json");
198
199 let result = ecosystem.parse_manifest("{invalid json}", &uri).await;
200 assert!(result.is_err());
201 }
202
203 #[tokio::test]
204 async fn test_package_name_completion_context_has_real_range() {
205 let cache = Arc::new(deps_core::HttpCache::new());
208 let ecosystem = ComposerEcosystem::new(cache);
209 let content = "{\n \"require\": {\n \"symfony/console\": \"^6.0\"\n }\n}";
210 let uri = deps_core::test_util::test_uri("/test/composer.json");
211
212 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
213 let position = Position::new(2, 9); let context = deps_core::completion::detect_completion_context(
216 parse_result.as_ref(),
217 position,
218 content,
219 );
220
221 match context {
222 deps_core::completion::CompletionContext::PackageName { prefix, range } => {
223 assert_eq!(prefix, "symf");
224 assert_ne!(range, Range::default());
225 assert_eq!(range, Range::new(Position::new(2, 5), Position::new(2, 20)));
226 }
227 other => panic!("Expected PackageName context, got {other:?}"),
228 }
229 }
230
231 #[tokio::test]
232 async fn test_complete_package_names_short_prefix() {
233 let cache = Arc::new(deps_core::HttpCache::new());
234 let ecosystem = ComposerEcosystem::new(cache);
235
236 let results = ecosystem
237 .complete_package_names("s", Range::default())
238 .await;
239 assert!(results.is_empty());
240 }
241
242 #[tokio::test]
243 async fn test_generate_inlay_hints_empty() {
244 let cache = Arc::new(deps_core::HttpCache::new());
245 let ecosystem = ComposerEcosystem::new(cache);
246 let uri = deps_core::test_util::test_uri("/test/composer.json");
247
248 let content = r#"{"require": {}}"#;
249 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
250
251 let hints = ecosystem
252 .generate_inlay_hints(
253 parse_result.as_ref(),
254 VersionData::new(&HashMap::new(), &HashMap::new()),
255 deps_core::LoadingState::Loaded,
256 &EcosystemConfig::default(),
257 )
258 .await;
259
260 assert!(hints.is_empty());
261 }
262
263 #[tokio::test]
264 async fn test_generate_completions_no_context() {
265 let cache = Arc::new(deps_core::HttpCache::new());
266 let ecosystem = ComposerEcosystem::new(cache);
267 let uri = deps_core::test_util::test_uri("/test/composer.json");
268
269 let content = r#"{"name": "test/project"}"#;
270 let parse_result = ecosystem.parse_manifest(content, &uri).await.unwrap();
271 let position = Position {
272 line: 0,
273 character: 0,
274 };
275
276 let completions = ecosystem
277 .generate_completions(
278 parse_result.as_ref(),
279 position,
280 content,
281 deps_core::FreshnessSettings::default(),
282 )
283 .await;
284 assert!(completions.items.is_empty());
285 }
286}