1use std::collections::HashMap;
2use std::sync::Arc;
3
4use serde_json::json;
5use tokio::sync::RwLock;
6use tower_lsp::jsonrpc::Result;
7use tower_lsp::lsp_types::*;
8use tower_lsp::{Client, LanguageServer, LspService, Server};
9
10use crate::color::get_catalog_color;
11use crate::document::{Document, position_in_range};
12use crate::parser::parse_package_dependencies;
13use crate::workspace::WorkspaceManager;
14
15pub async fn run_stdio_server() {
16 let stdin = tokio::io::stdin();
17 let stdout = tokio::io::stdout();
18
19 let documents = Arc::new(RwLock::new(HashMap::new()));
20 let documents_for_service = Arc::clone(&documents);
21 let (service, socket) = LspService::new(move |client| Backend {
22 client,
23 documents: documents_for_service,
24 workspace: WorkspaceManager::new(Arc::clone(&documents)),
25 });
26
27 Server::new(stdin, stdout, socket).serve(service).await;
28}
29
30pub struct Backend {
31 client: Client,
32 documents: Arc<RwLock<HashMap<Url, Document>>>,
33 workspace: WorkspaceManager,
34}
35
36#[tower_lsp::async_trait]
37impl LanguageServer for Backend {
38 async fn initialize(&self, params: InitializeParams) -> Result<InitializeResult> {
39 if let Some(folders) = params.workspace_folders {
40 self.workspace
41 .set_workspace_folders(folders.into_iter().map(|folder| folder.uri).collect())
42 .await;
43 }
44
45 Ok(InitializeResult {
46 capabilities: ServerCapabilities {
47 text_document_sync: Some(TextDocumentSyncCapability::Kind(
48 TextDocumentSyncKind::INCREMENTAL,
49 )),
50 code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
51 inlay_hint_provider: Some(OneOf::Left(true)),
52 code_lens_provider: Some(CodeLensOptions {
53 resolve_provider: Some(false),
54 }),
55 hover_provider: Some(HoverProviderCapability::Simple(true)),
56 definition_provider: Some(OneOf::Left(true)),
57 workspace: Some(WorkspaceServerCapabilities {
58 workspace_folders: Some(WorkspaceFoldersServerCapabilities {
59 supported: Some(true),
60 change_notifications: Some(OneOf::Left(true)),
61 }),
62 file_operations: None,
63 }),
64 ..ServerCapabilities::default()
65 },
66 server_info: Some(ServerInfo {
67 name: "package-json-lsp".to_string(),
68 version: Some(env!("CARGO_PKG_VERSION").to_string()),
69 }),
70 })
71 }
72
73 async fn initialized(&self, _: InitializedParams) {
74 self.client
75 .log_message(MessageType::INFO, "package-json-lsp initialized")
76 .await;
77 }
78
79 async fn shutdown(&self) -> Result<()> {
80 Ok(())
81 }
82
83 async fn did_open(&self, params: DidOpenTextDocumentParams) {
84 let doc = Document::new(
85 params.text_document.uri.clone(),
86 params.text_document.version,
87 params.text_document.text,
88 );
89 self.documents
90 .write()
91 .await
92 .insert(params.text_document.uri.clone(), doc);
93 self.send_diagnostics(params.text_document.uri).await;
94 }
95
96 async fn did_change(&self, params: DidChangeTextDocumentParams) {
97 if let Some(doc) = self
98 .documents
99 .write()
100 .await
101 .get_mut(¶ms.text_document.uri)
102 {
103 doc.version = params.text_document.version;
104 doc.apply_changes(params.content_changes);
105 }
106 self.workspace
107 .clear_document_caches(¶ms.text_document.uri)
108 .await;
109 }
110
111 async fn did_save(&self, params: DidSaveTextDocumentParams) {
112 self.workspace
113 .clear_document_caches(¶ms.text_document.uri)
114 .await;
115 self.send_diagnostics(params.text_document.uri).await;
116 }
117
118 async fn did_close(&self, params: DidCloseTextDocumentParams) {
119 self.documents
120 .write()
121 .await
122 .remove(¶ms.text_document.uri);
123 self.client
124 .publish_diagnostics(params.text_document.uri, Vec::new(), None)
125 .await;
126 }
127
128 async fn inlay_hint(&self, params: InlayHintParams) -> Result<Option<Vec<InlayHint>>> {
129 let Some(document) = self.package_document(¶ms.text_document.uri).await else {
130 return Ok(None);
131 };
132
133 let deps = parse_package_dependencies(&document);
134 let mut hints = Vec::new();
135 for dep in deps {
136 let Some(catalog) = dep.catalog else {
137 continue;
138 };
139 let Some(result) = self
140 .workspace
141 .resolve_catalog(&document.uri, &dep.package_name, &catalog)
142 .await
143 else {
144 continue;
145 };
146 let color_key = if catalog == "default" {
147 "default".to_string()
148 } else {
149 format!("{catalog}-lens")
150 };
151 hints.push(InlayHint {
152 position: dep.value_range.end,
153 label: InlayHintLabel::String(result.version),
154 kind: Some(InlayHintKind::TYPE),
155 text_edits: None,
156 tooltip: None,
157 padding_left: None,
158 padding_right: None,
159 data: Some(json!({
160 "catalog": catalog,
161 "color": get_catalog_color(&color_key),
162 })),
163 });
164 }
165
166 Ok(Some(hints))
167 }
168
169 async fn hover(&self, params: HoverParams) -> Result<Option<Hover>> {
170 let Some(document) = self
171 .package_document(¶ms.text_document_position_params.text_document.uri)
172 .await
173 else {
174 return Ok(None);
175 };
176 let position = params.text_document_position_params.position;
177 let deps = parse_package_dependencies(&document);
178
179 for dep in &deps {
180 let Some(catalog) = &dep.catalog else {
181 continue;
182 };
183 if position_in_range(position, dep.value_range)
184 && let Some(result) = self
185 .workspace
186 .resolve_catalog(&document.uri, &dep.package_name, catalog)
187 .await
188 {
189 return Ok(Some(Hover {
190 contents: HoverContents::Markup(MarkupContent {
191 kind: MarkupKind::Markdown,
192 value: format!(
193 "- {} Catalog: `{}`\n- Version: `{}`",
194 result.manager.as_str(),
195 catalog,
196 result.version
197 ),
198 }),
199 range: Some(dep.value_range),
200 }));
201 }
202 }
203
204 for dep in deps {
205 if position_in_range(position, dep.property_range) {
206 let Some(outdated) = self
207 .workspace
208 .resolve_version(&document.uri, &dep.package_name)
209 .await
210 else {
211 return Ok(Some(Hover {
212 contents: HoverContents::Markup(MarkupContent {
213 kind: MarkupKind::Markdown,
214 value: "latest version".to_string(),
215 }),
216 range: Some(dep.property_range),
217 }));
218 };
219
220 return Ok(Some(Hover {
221 contents: HoverContents::Markup(MarkupContent {
222 kind: MarkupKind::Markdown,
223 value: format!(
224 "- Wanted: `{}`\n- Latest: `{}`",
225 outdated.wanted, outdated.latest
226 ),
227 }),
228 range: Some(dep.property_range),
229 }));
230 }
231 }
232
233 Ok(None)
234 }
235
236 async fn goto_definition(
237 &self,
238 params: GotoDefinitionParams,
239 ) -> Result<Option<GotoDefinitionResponse>> {
240 let Some(document) = self
241 .package_document(¶ms.text_document_position_params.text_document.uri)
242 .await
243 else {
244 return Ok(None);
245 };
246 let position = params.text_document_position_params.position;
247
248 for dep in parse_package_dependencies(&document) {
249 if !position_in_range(position, dep.value_range) {
250 continue;
251 }
252 if let Some(catalog) = dep.catalog
253 && let Some(result) = self
254 .workspace
255 .resolve_catalog(&document.uri, &dep.package_name, &catalog)
256 .await
257 && let Some(definition) = result.definition
258 {
259 return Ok(Some(GotoDefinitionResponse::Scalar(definition)));
260 }
261 if dep.is_workspace_ref
262 && let Some(result) = self
263 .workspace
264 .resolve_workspace_package(&document.uri, &dep.package_name)
265 .await
266 {
267 return Ok(Some(GotoDefinitionResponse::Scalar(result.definition)));
268 }
269 }
270
271 Ok(None)
272 }
273
274 async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
275 let Some(document) = self.package_document(¶ms.text_document.uri).await else {
276 return Ok(None);
277 };
278
279 let mut lenses = Vec::new();
280 for dep in parse_package_dependencies(&document) {
281 let Some(outdated) = self
282 .workspace
283 .resolve_version(&document.uri, &dep.package_name)
284 .await
285 else {
286 continue;
287 };
288
289 let version = if let Some(catalog) = &dep.catalog {
290 self.workspace
291 .resolve_catalog(&document.uri, &dep.package_name, catalog)
292 .await
293 .map(|result| result.version)
294 .unwrap_or_else(|| dep.version_string.clone())
295 } else {
296 dep.version_string.clone()
297 };
298 let prefix = version_prefix(&version);
299 let title = if outdated.current == outdated.wanted {
300 format!("Latest: {prefix}{}", outdated.latest)
301 } else {
302 format!(
303 "Wanted: {prefix}{} | Latest: {prefix}{}",
304 outdated.wanted, outdated.latest
305 )
306 };
307
308 lenses.push(CodeLens {
309 range: dep.value_range,
310 command: Some(Command {
311 title,
312 command: "package-json-lsp:update".to_string(),
313 arguments: None,
314 }),
315 data: None,
316 });
317 }
318
319 Ok(Some(lenses))
320 }
321
322 async fn code_action(&self, params: CodeActionParams) -> Result<Option<CodeActionResponse>> {
323 let Some(document) = self.package_document(¶ms.text_document.uri).await else {
324 return Ok(None);
325 };
326
327 for dep in parse_package_dependencies(&document) {
328 if !position_in_range(params.range.start, dep.value_range)
329 && !position_in_range(params.range.end, dep.value_range)
330 {
331 continue;
332 }
333 let Some(outdated) = self
334 .workspace
335 .resolve_version(&document.uri, &dep.package_name)
336 .await
337 else {
338 return Ok(None);
339 };
340
341 let (uri, range, version) = if let Some(catalog) = &dep.catalog {
342 let Some(catalog_info) = self
343 .workspace
344 .resolve_catalog(&document.uri, &dep.package_name, catalog)
345 .await
346 else {
347 return Ok(None);
348 };
349 let Some(definition) = catalog_info.definition else {
350 return Ok(None);
351 };
352 (definition.uri, definition.range, catalog_info.version)
353 } else {
354 (
355 document.uri.clone(),
356 dep.value_range,
357 dep.version_string.clone(),
358 )
359 };
360
361 let prefix = version_prefix(&version);
362 let new_text = format!("{prefix}{}", outdated.latest);
363 let edit = WorkspaceEdit {
364 changes: Some(HashMap::from([(
365 uri,
366 vec![TextEdit {
367 range,
368 new_text: new_text.clone(),
369 }],
370 )])),
371 document_changes: None,
372 change_annotations: None,
373 };
374
375 return Ok(Some(vec![CodeActionOrCommand::CodeAction(CodeAction {
376 title: format!(
377 "Update {} to latest version ({})",
378 dep.package_name, new_text
379 ),
380 kind: None,
381 diagnostics: Some(
382 params
383 .context
384 .diagnostics
385 .into_iter()
386 .filter(|diagnostic| {
387 diagnostic.source.as_deref() == Some("pnpm")
388 && diagnostic.code.as_ref().is_some_and(|code| match code {
389 NumberOrString::String(value) => value == "outdated",
390 NumberOrString::Number(_) => false,
391 })
392 })
393 .collect(),
394 ),
395 edit: Some(edit),
396 command: None,
397 is_preferred: None,
398 disabled: None,
399 data: None,
400 })]));
401 }
402
403 Ok(None)
404 }
405}
406
407impl Backend {
408 async fn package_document(&self, uri: &Url) -> Option<Document> {
409 if !uri.path().ends_with("package.json") {
410 return None;
411 }
412 self.documents.read().await.get(uri).cloned()
413 }
414
415 async fn send_diagnostics(&self, uri: Url) {
416 let Some(document) = self.package_document(&uri).await else {
417 return;
418 };
419 let mut diagnostics = Vec::new();
420
421 for dep in parse_package_dependencies(&document) {
422 let Some(outdated) = self
423 .workspace
424 .resolve_version(&document.uri, &dep.package_name)
425 .await
426 else {
427 continue;
428 };
429
430 diagnostics.push(Diagnostic {
431 range: dep.value_range,
432 severity: Some(DiagnosticSeverity::INFORMATION),
433 code: Some(NumberOrString::String("outdated".to_string())),
434 code_description: None,
435 source: Some("pnpm".to_string()),
436 message: format!(
437 "- Wanted: `{}`\n- Latest: `{}`",
438 outdated.wanted, outdated.latest
439 ),
440 related_information: None,
441 tags: outdated
442 .is_deprecated
443 .then_some(vec![DiagnosticTag::DEPRECATED]),
444 data: None,
445 });
446 }
447
448 self.client
449 .publish_diagnostics(uri, diagnostics, Some(document.version))
450 .await;
451 }
452}
453
454fn version_prefix(version: &str) -> &str {
455 if version.starts_with(">=") {
456 ">="
457 } else if version.starts_with(['~', '^', '>']) {
458 &version[..1]
459 } else {
460 ""
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn preserves_version_prefixes() {
470 assert_eq!(version_prefix("^1.0.0"), "^");
471 assert_eq!(version_prefix("~1.0.0"), "~");
472 assert_eq!(version_prefix(">1.0.0"), ">");
473 assert_eq!(version_prefix(">=1.0.0"), ">=");
474 assert_eq!(version_prefix("1.0.0"), "");
475 }
476}