1use crate::constants::SystemdConstants;
2use crate::parser::SystemdParser;
3use log::{debug, trace};
4use std::collections::HashMap;
5use tower_lsp_server::lsp_types::{
6 CompletionItem, CompletionItemKind, CompletionResponse, Documentation, MarkupContent,
7 MarkupKind, Position, Uri,
8};
9
10#[derive(Debug)]
11pub struct SystemdCompletion {
12 section_completions: Vec<CompletionItem>,
13 directive_completions: HashMap<String, Vec<CompletionItem>>,
14}
15
16#[derive(Debug, Clone)]
17enum CompletionContext {
18 SectionHeader,
19 Directive(String),
20 Value { section: String, directive: String },
21 Global,
22}
23
24impl SystemdCompletion {
25 pub fn new() -> Self {
26 let mut section_completions = Vec::new();
27 for (name, description) in SystemdConstants::section_documentation() {
28 let documentation = Self::create_documentation(
29 &format!("[{}] Section", name),
30 description,
31 &format!("systemd.{}.5", name.to_lowercase()),
32 );
33
34 section_completions.push(Self::create_completion_item(
35 format!("[{}]", name),
36 CompletionItemKind::MODULE,
37 format!("systemd {} section", name.to_lowercase()),
38 documentation,
39 Some(format!("[{}]", name)),
40 ));
41 }
42
43 let mut directive_completions = HashMap::new();
44 let directive_descriptions = SystemdConstants::directive_descriptions();
45
46 for (section, directives) in SystemdConstants::section_directives() {
47 let mut completion_items = Vec::new();
48 for directive in directives {
49 let description = directive_descriptions
50 .get(&(section, directive))
51 .unwrap_or(&"systemd directive")
52 .to_string();
53 completion_items.push(Self::create_directive_completion(
54 section,
55 directive,
56 &description,
57 ));
58 }
59 directive_completions.insert(section.to_string(), completion_items);
60 }
61
62 Self {
63 section_completions,
64 directive_completions,
65 }
66 }
67
68 pub async fn get_completions(
69 &self,
70 parser: &SystemdParser,
71 uri: &Uri,
72 position: &Position,
73 ) -> Option<CompletionResponse> {
74 trace!(
75 "Generating completions for {}:{} in {:?}",
76 position.line,
77 position.character,
78 uri
79 );
80
81 let unit = parser.get_parsed_document(uri)?;
83 let document_text = parser.get_document_text(uri)?;
84
85 let completion_context = self.determine_context(parser, &unit, position, &document_text);
87
88 debug!("Completion context: {:?}", completion_context);
89
90 match completion_context {
91 CompletionContext::SectionHeader => {
92 debug!("Providing section completions");
94 Some(CompletionResponse::Array(self.section_completions.clone()))
95 }
96 CompletionContext::Directive(section_name) => {
97 debug!(
99 "Providing directive completions for section: {}",
100 section_name
101 );
102 if let Some(directives) = self.directive_completions.get(§ion_name) {
103 Some(CompletionResponse::Array(directives.clone()))
104 } else {
105 debug!("No directives found for section: {}", section_name);
106 Some(CompletionResponse::Array(Vec::new()))
107 }
108 }
109 CompletionContext::Value {
110 section: section_name,
111 directive,
112 } => {
113 debug!(
114 "Providing value completions for {}.{}",
115 section_name, directive
116 );
117 match self.get_value_completions(section_name.as_str(), directive.as_str()) {
118 Some(items) if !items.is_empty() => Some(CompletionResponse::Array(items)),
119 _ => {
120 debug!(
121 "No value completions available for {}.{}",
122 section_name, directive
123 );
124 None
125 }
126 }
127 }
128 CompletionContext::Global => {
129 debug!("Providing global completions (sections)");
131 Some(CompletionResponse::Array(self.section_completions.clone()))
132 }
133 }
134 }
135
136 fn determine_context(
137 &self,
138 parser: &SystemdParser,
139 unit: &crate::parser::SystemdUnit,
140 position: &Position,
141 document_text: &str,
142 ) -> CompletionContext {
143 let lines: Vec<&str> = document_text.lines().collect();
144 let current_line_index = position.line as usize;
145
146 if current_line_index >= lines.len() {
148 return CompletionContext::Global;
149 }
150
151 let current_line = lines[current_line_index];
152 let character_position = position.character as usize;
153
154 if character_position == 0 || current_line.trim_start().starts_with('[') {
156 if current_line.trim().starts_with('[')
158 || (character_position > 0
159 && current_line
160 .chars()
161 .take(character_position)
162 .collect::<String>()
163 .trim()
164 .starts_with('['))
165 {
166 return CompletionContext::SectionHeader;
167 }
168 }
169
170 if let Some(_section_name) = parser.get_section_header_at_position(unit, position) {
172 return CompletionContext::SectionHeader;
173 }
174
175 if let Some(section) = parser.get_section_at_line(unit, position.line) {
177 if let Some(eq_idx) = current_line.find('=') {
179 let eq_char_index = current_line[..eq_idx].chars().count() as u32;
180 if position.character > eq_char_index {
181 let key = current_line[..eq_idx].trim();
182 if !key.is_empty() {
183 return CompletionContext::Value {
184 section: section.name.clone(),
185 directive: key.to_string(),
186 };
187 }
188 }
189 }
190
191 if let Some(directive) = section.directives.iter().find(|directive| {
193 directive.value_spans.iter().any(|span| {
194 span.line == position.line
195 && (span.line != directive.line_number || position.character >= span.start)
196 })
197 }) {
198 return CompletionContext::Value {
199 section: section.name.clone(),
200 directive: directive.key.clone(),
201 };
202 }
203
204 return CompletionContext::Directive(section.name.clone());
205 }
206
207 CompletionContext::Global
209 }
210
211 fn get_value_completions(
212 &self,
213 section_name: &str,
214 directive_name: &str,
215 ) -> Option<Vec<CompletionItem>> {
216 let section_map = SystemdConstants::section_directives();
217 let canonical_section = section_map
218 .keys()
219 .find(|name| name.eq_ignore_ascii_case(section_name))
220 .copied()
221 .unwrap_or(section_name);
222
223 let canonical_directive = section_map
224 .get(canonical_section)
225 .and_then(|directives| {
226 directives
227 .iter()
228 .find(|entry| entry.eq_ignore_ascii_case(directive_name))
229 .copied()
230 })
231 .or_else(|| {
232 let global_values = SystemdConstants::valid_values();
233 global_values
234 .keys()
235 .find(|key| key.eq_ignore_ascii_case(directive_name))
236 .copied()
237 })
238 .unwrap_or(directive_name);
239
240 let values =
241 SystemdConstants::valid_values_for_section(canonical_section, canonical_directive)?;
242
243 if values.is_empty() {
244 return None;
245 }
246
247 let items = values
248 .iter()
249 .map(|value| {
250 Self::create_value_completion(canonical_section, canonical_directive, value)
251 })
252 .collect::<Vec<_>>();
253
254 Some(items)
255 }
256
257 fn create_documentation(title: &str, description: &str, reference: &str) -> Documentation {
258 Documentation::MarkupContent(MarkupContent {
259 kind: MarkupKind::Markdown,
260 value: format!(
261 "**{}**\n\n{}\n\n---\n*Reference: {}*",
262 title, description, reference
263 ),
264 })
265 }
266
267 fn create_completion_item(
268 label: String,
269 kind: CompletionItemKind,
270 detail: String,
271 documentation: Documentation,
272 insert_text: Option<String>,
273 ) -> CompletionItem {
274 CompletionItem {
275 label,
276 label_details: None,
277 kind: Some(kind),
278 detail: Some(detail),
279 documentation: Some(documentation),
280 deprecated: None,
281 preselect: None,
282 sort_text: None,
283 filter_text: None,
284 insert_text,
285 insert_text_format: None,
286 insert_text_mode: None,
287 text_edit: None,
288 additional_text_edits: None,
289 command: None,
290 commit_characters: None,
291 data: None,
292 tags: None,
293 }
294 }
295
296 fn extract_directive_from_markdown(section_name: &str, directive_name: &str) -> Option<String> {
303 fn search_in_markdown(markdown_content: &str, directive_name: &str) -> Option<String> {
305 let directive_header = format!("### {}=", directive_name);
306 let directive_header_lower = directive_header.to_lowercase();
307
308 let lines = markdown_content.lines();
309 let mut found_header = false;
310 let mut doc_lines = Vec::new();
311
312 for line in lines {
313 if line.to_lowercase() == directive_header_lower {
314 found_header = true;
315 continue;
316 }
317
318 if found_header {
319 if line.starts_with("### ") || line.starts_with("## ") {
321 break;
322 }
323 doc_lines.push(line);
324 }
325 }
326
327 if doc_lines.is_empty() {
328 return None;
329 }
330
331 let documentation = doc_lines.join("\n").trim().to_string();
333
334 let documentation = if let Some(last_ref_pos) = documentation.rfind("**Reference:**") {
336 documentation[..last_ref_pos].trim().to_string()
337 } else {
338 documentation
339 };
340
341 Some(documentation)
342 }
343
344 let section_docs = SystemdConstants::section_documentation();
346 if let Some(section_key) = section_docs.keys().find(|k| k.eq_ignore_ascii_case(section_name)) {
347 if let Some(markdown_content) = section_docs.get(section_key) {
348 if let Some(result) = search_in_markdown(markdown_content, directive_name) {
349 return Some(result);
350 }
351 }
352 }
353
354 let shared_docs_keys = SystemdConstants::section_shared_docs(section_name);
357 if !shared_docs_keys.is_empty() {
358 let shared_docs = SystemdConstants::shared_documentation();
359 for shared_key in shared_docs_keys {
360 if let Some(shared_content) = shared_docs.get(shared_key) {
361 if let Some(result) = search_in_markdown(shared_content, directive_name) {
362 return Some(result);
363 }
364 }
365 }
366 }
367
368 None
369 }
370
371 fn create_directive_completion(
373 section: &str,
374 key: &str,
375 short_description: &str,
376 ) -> CompletionItem {
377 let documentation =
379 if let Some(markdown_doc) = Self::extract_directive_from_markdown(section, key) {
380 Documentation::MarkupContent(MarkupContent {
381 kind: MarkupKind::Markdown,
382 value: format!("**{}**\n\n{}", key, markdown_doc),
383 })
384 } else {
385 Self::create_documentation(key, short_description, "systemd documentation")
387 };
388
389 Self::create_completion_item(
390 key.to_string(),
391 CompletionItemKind::PROPERTY,
392 "systemd directive".to_string(),
393 documentation,
394 Some(format!("{}=", key)),
395 )
396 }
397
398 fn extract_value_documentation(
404 section_name: &str,
405 directive_name: &str,
406 value: &str,
407 ) -> Option<String> {
408 let directive_doc = Self::extract_directive_from_markdown(section_name, directive_name)?;
410
411 let value_lower = value.to_lowercase();
414
415 for line in directive_doc.lines() {
416 let line_trimmed = line.trim();
417 if !line_trimmed.starts_with("- **") {
418 continue;
419 }
420
421 let after_bullets = line_trimmed.trim_start_matches("- **");
423
424 let value_end = after_bullets.find("**").unwrap_or(0);
426 if value_end == 0 {
427 continue;
428 }
429
430 let documented_value = &after_bullets[..value_end];
431
432 if documented_value.to_lowercase().starts_with(&value_lower)
434 || value_lower.starts_with(&documented_value.to_lowercase())
435 {
436 let rest = &after_bullets[value_end..];
438 if let Some(desc_start) = rest.find(':') {
439 let description = rest[desc_start + 1..].trim();
440 if !description.is_empty() {
441 return Some(description.to_string());
442 }
443 }
444 }
445 }
446
447 None
448 }
449
450 fn create_value_completion(section: &str, directive: &str, value: &str) -> CompletionItem {
452 let documentation =
454 if let Some(value_doc) = Self::extract_value_documentation(section, directive, value) {
455 Documentation::MarkupContent(MarkupContent {
456 kind: MarkupKind::Markdown,
457 value: format!("**{}**\n\n{}", value, value_doc),
458 })
459 } else {
460 Documentation::MarkupContent(MarkupContent {
461 kind: MarkupKind::Markdown,
462 value: format!("Valid `{}` option for `{}`", value, directive),
463 })
464 };
465
466 Self::create_completion_item(
467 value.to_string(),
468 CompletionItemKind::VALUE,
469 format!("{} value", directive),
470 documentation,
471 Some(value.to_string()),
472 )
473 }
474
475 pub fn get_section_documentation(&self, section_name: &str) -> Option<String> {
476 SystemdConstants::section_documentation()
477 .get(section_name)
478 .map(|description| {
479 format!(
480 "**[{}] Section**\n\n{}\n\n**Reference:** systemd.{}.5",
481 section_name,
482 description,
483 section_name.to_lowercase()
484 )
485 })
486 }
487
488 pub fn get_directive_documentation(
489 &self,
490 directive_name: &str,
491 section_name: &str,
492 ) -> Option<String> {
493 Self::extract_directive_from_markdown(section_name, directive_name)
494 }
495}
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500 use tower_lsp_server::lsp_types::{Position, Uri};
501
502 #[tokio::test]
503 async fn test_completion_creation() {
504 let completion = SystemdCompletion::new();
505
506 assert!(!completion.section_completions.is_empty());
508
509 assert!(completion.directive_completions.contains_key("Unit"));
511 assert!(completion.directive_completions.contains_key("Service"));
512 assert!(completion.directive_completions.contains_key("Install"));
513 }
514
515 #[tokio::test]
516 async fn test_get_completions_returns_results() {
517 let completion = SystemdCompletion::new();
518 let parser = SystemdParser::new();
519 let uri = "file:///test.service".parse::<Uri>().unwrap();
520 let position = Position::new(0, 0);
521
522 let document_text = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
524 parser.update_document(&uri, document_text);
525
526 let result = completion.get_completions(&parser, &uri, &position).await;
527
528 assert!(result.is_some());
529 if let Some(CompletionResponse::Array(items)) = result {
530 assert!(!items.is_empty());
531 assert!(items.iter().any(|item| item.label == "[Unit]"));
533 assert!(items.iter().any(|item| item.label == "[Service]"));
534 assert!(items.iter().any(|item| item.label == "[Install]"));
535 }
536 }
537
538 #[tokio::test]
539 async fn test_completion_item_properties() {
540 let completion = SystemdCompletion::new();
541 let parser = SystemdParser::new();
542 let uri = "file:///test.service".parse::<Uri>().unwrap();
543 let position = Position::new(0, 0);
544
545 let document_text = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
547 parser.update_document(&uri, document_text);
548
549 let result = completion.get_completions(&parser, &uri, &position).await;
550
551 if let Some(CompletionResponse::Array(items)) = result {
552 let section_item = items.iter().find(|item| item.label == "[Unit]").unwrap();
554 assert_eq!(section_item.kind, Some(CompletionItemKind::MODULE));
555 assert!(section_item.detail.is_some());
556 assert!(section_item.documentation.is_some());
557 }
558 }
559
560 #[test]
561 fn test_create_documentation() {
562 let doc = SystemdCompletion::create_documentation(
563 "Test Title",
564 "Test description",
565 "test.reference",
566 );
567
568 if let Documentation::MarkupContent(content) = doc {
569 assert_eq!(content.kind, MarkupKind::Markdown);
570 assert!(content.value.contains("**Test Title**"));
571 assert!(content.value.contains("Test description"));
572 assert!(content.value.contains("test.reference"));
573 } else {
574 panic!("Expected MarkupContent documentation");
575 }
576 }
577
578 #[test]
579 fn test_create_directive_completion() {
580 let completion =
581 SystemdCompletion::create_directive_completion("Service", "Type", "Test description");
582
583 assert_eq!(completion.label, "Type");
584 assert_eq!(completion.kind, Some(CompletionItemKind::PROPERTY));
585 assert_eq!(completion.detail, Some("systemd directive".to_string()));
586 assert_eq!(completion.insert_text, Some("Type=".to_string()));
587 assert!(completion.documentation.is_some());
588 }
589
590 #[test]
591 fn test_get_section_documentation() {
592 let completion = SystemdCompletion::new();
593
594 let doc = completion.get_section_documentation("Unit");
595 assert!(doc.is_some());
596
597 let doc_content = doc.unwrap();
598 assert!(doc_content.contains("**[Unit] Section**"));
599 assert!(doc_content.contains("**Reference:** systemd.unit.5"));
600
601 let no_doc = completion.get_section_documentation("NonExistentSection");
603 assert!(no_doc.is_none());
604 }
605
606 #[test]
607 fn test_get_directive_documentation() {
608 let completion = SystemdCompletion::new();
609
610 let desc_doc = completion.get_directive_documentation("description", "Unit");
612 assert!(desc_doc.is_some());
613
614 let type_doc = completion.get_directive_documentation("type", "Service");
615 assert!(type_doc.is_some());
616
617 let desc_doc_upper = completion.get_directive_documentation("DESCRIPTION", "Unit");
619 assert!(desc_doc_upper.is_some());
620 assert_eq!(desc_doc, desc_doc_upper);
621
622 let no_doc = completion.get_directive_documentation("NonExistent", "Unit");
624 assert!(no_doc.is_none());
625 }
626
627 #[tokio::test]
628 async fn test_no_duplicate_completions() {
629 let completion = SystemdCompletion::new();
630 let parser = SystemdParser::new();
631 let uri = "file:///test.service".parse::<Uri>().unwrap();
632 let position = Position::new(0, 0);
633
634 let document_text = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
636 parser.update_document(&uri, document_text);
637
638 let result = completion.get_completions(&parser, &uri, &position).await;
639
640 if let Some(CompletionResponse::Array(items)) = result {
641 let mut labels = std::collections::HashSet::new();
642 let mut duplicates = Vec::new();
643
644 for item in &items {
645 if !labels.insert(&item.label) {
646 duplicates.push(&item.label);
647 }
648 }
649
650 assert!(
651 duplicates.is_empty(),
652 "Found duplicate completion labels: {:?}",
653 duplicates
654 );
655 }
656 }
657
658 #[tokio::test]
659 async fn test_context_aware_completions() {
660 let completion = SystemdCompletion::new();
661 let parser = SystemdParser::new();
662 let uri = "file:///test.service".parse::<Uri>().unwrap();
663
664 let document_text = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
666 parser.update_document(&uri, document_text);
667
668 let global_result = completion
670 .get_completions(&parser, &uri, &Position::new(0, 0))
671 .await;
672 if let Some(CompletionResponse::Array(items)) = global_result {
673 assert!(items.iter().any(|item| item.label == "[Unit]"));
675 assert!(items.iter().any(|item| item.label == "[Service]"));
676 assert!(!items.iter().any(|item| item.label == "Description"));
678 assert!(!items.iter().any(|item| item.label == "Type"));
679 }
680
681 let unit_result = completion
683 .get_completions(&parser, &uri, &Position::new(1, 0))
684 .await;
685 if let Some(CompletionResponse::Array(items)) = unit_result {
686 assert!(items.iter().any(|item| item.label == "Description"));
688 assert!(items.iter().any(|item| item.label == "Documentation"));
689 assert!(!items.iter().any(|item| item.label == "Type"));
691 assert!(!items.iter().any(|item| item.label == "ExecStart"));
692 }
693
694 let service_result = completion
696 .get_completions(&parser, &uri, &Position::new(4, 0))
697 .await;
698 if let Some(CompletionResponse::Array(items)) = service_result {
699 assert!(items.iter().any(|item| item.label == "Type"));
701 assert!(items.iter().any(|item| item.label == "ExecStart"));
702 assert!(!items.iter().any(|item| item.label == "Documentation"));
704 }
705 }
706
707 #[tokio::test]
708 async fn test_section_header_completion() {
709 let completion = SystemdCompletion::new();
710 let parser = SystemdParser::new();
711 let uri = "file:///test.service".parse::<Uri>().unwrap();
712
713 let document_text = "[Un";
715 parser.update_document(&uri, document_text);
716
717 let result = completion
719 .get_completions(&parser, &uri, &Position::new(0, 3))
720 .await;
721 if let Some(CompletionResponse::Array(items)) = result {
722 assert!(items.iter().any(|item| item.label == "[Unit]"));
724 assert!(items.iter().any(|item| item.label == "[Service]"));
725 assert!(!items.iter().any(|item| item.label == "Description"));
727 assert!(!items.iter().any(|item| item.label == "Type"));
728 }
729 }
730
731 #[tokio::test]
732 async fn test_value_completions_for_restart_directive() {
733 let completion = SystemdCompletion::new();
734 let parser = SystemdParser::new();
735 let uri = "file:///value-test.service".parse::<Uri>().unwrap();
736
737 let document_text = "[Service]\nRestart=\n";
738 parser.update_document(&uri, document_text);
739
740 let cursor = "Restart=".chars().count() as u32;
741 let result = completion
742 .get_completions(&parser, &uri, &Position::new(1, cursor))
743 .await;
744
745 if let Some(CompletionResponse::Array(items)) = result {
746 assert!(items.iter().any(|item| item.label == "no"));
747 assert!(items.iter().any(|item| item.label == "always"));
748 assert!(items
749 .iter()
750 .all(|item| item.kind == Some(CompletionItemKind::VALUE)));
751 } else {
752 panic!("Expected value completions for Restart directive");
753 }
754 }
755
756 #[tokio::test]
757 async fn test_no_value_completions_for_freeform_directive() {
758 let completion = SystemdCompletion::new();
759 let parser = SystemdParser::new();
760 let uri = "file:///value-none.service".parse::<Uri>().unwrap();
761
762 let document_text = "[Unit]\nDescription=\n";
763 parser.update_document(&uri, document_text);
764
765 let cursor = "Description=".chars().count() as u32;
766 let result = completion
767 .get_completions(&parser, &uri, &Position::new(1, cursor))
768 .await;
769
770 assert!(
771 result.is_none(),
772 "Expected no completions for freeform directive value"
773 );
774 }
775}