systemd_lsp/
completion.rs

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        // Get the parsed document and document text
82        let unit = parser.get_parsed_document(uri)?;
83        let document_text = parser.get_document_text(uri)?;
84
85        // Determine the context at the current position
86        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                // Only show section completions
93                debug!("Providing section completions");
94                Some(CompletionResponse::Array(self.section_completions.clone()))
95            }
96            CompletionContext::Directive(section_name) => {
97                // Only show directives for the current section
98                debug!(
99                    "Providing directive completions for section: {}",
100                    section_name
101                );
102                if let Some(directives) = self.directive_completions.get(&section_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                // Show section completions if we're not inside any section
130                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        // Check if we're beyond the document bounds
147        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        // Check if we're at the start of a line that begins with '[' or completing a section header
155        if character_position == 0 || current_line.trim_start().starts_with('[') {
156            // Check if the line starts with '[' - this indicates section header context
157            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        // Check if we're currently on a section header line
171        if let Some(_section_name) = parser.get_section_header_at_position(unit, position) {
172            return CompletionContext::SectionHeader;
173        }
174
175        // Check if we're inside a section (for directive completions)
176        if let Some(section) = parser.get_section_at_line(unit, position.line) {
177            // Check if cursor is positioned within the value part of a directive on the same line
178            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            // Detect multi-line or continuation value contexts using recorded spans
192            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        // Default to global context (show sections)
208        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    /*
297     * We Want to extract directive documentation from markdown files. To allow for
298     * deduplication and maintainability, each section has a comprehensive markdown file
299     * we require a strict format to adhere to discoverability of the documenation.
300     * Most notably, directive headers must be prefixed with "### " and suffixed with "=".
301     */
302    fn extract_directive_from_markdown(section_name: &str, directive_name: &str) -> Option<String> {
303        // Helper function to search for a directive in a markdown string
304        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                    // Stop at the next directive header (###) or section header (##)
320                    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            // Join the lines and trim
332            let documentation = doc_lines.join("\n").trim().to_string();
333
334            // Remove the trailing "**Reference:**" line if present
335            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        // First, try to find the directive in the section's own markdown file
345        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        // If not found in the section's own docs, search in shared documentation
355        // (exec, kill, resource-control) based on which section we're in
356        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    // We also leverage the directive completion for auto complete and hover.
372    fn create_directive_completion(
373        section: &str,
374        key: &str,
375        short_description: &str,
376    ) -> CompletionItem {
377        // Try to get comprehensive markdown documentation
378        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                // Fall back to short description
386                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    /* Extract value-specific documentation from the directive markdown. Similarly to directive
399     * being parsed from sections, we also use a format to find matches for value documentation.
400     * The prefix of "- **" is required, followed by the value name, and then either ":
401     * description".
402     */
403    fn extract_value_documentation(
404        section_name: &str,
405        directive_name: &str,
406        value: &str,
407    ) -> Option<String> {
408        // Get the full directive documentation
409        let directive_doc = Self::extract_directive_from_markdown(section_name, directive_name)?;
410
411        // Look for the value in bullet list format: - **value**: description
412        // or: - **value** (default): description
413        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            // Extract the value name from the bullet: - **value**: or - **value** (default):
422            let after_bullets = line_trimmed.trim_start_matches("- **");
423
424            // Find where the value name ends (could be **: or ** or **:)
425            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            // Check if this matches our value (case-insensitive, ignoring (default) etc)
433            if documented_value.to_lowercase().starts_with(&value_lower)
434                || value_lower.starts_with(&documented_value.to_lowercase())
435            {
436                // Extract the description after the **: or ):
437                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    // we want this for hover and autocomplete of values
451    fn create_value_completion(section: &str, directive: &str, value: &str) -> CompletionItem {
452        // Try to get value-specific documentation from markdown
453        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        // Test that sections are populated
507        assert!(!completion.section_completions.is_empty());
508
509        // Test that directive completions exist for main sections
510        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        // Add a basic document for testing
523        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            // At global level, should show section completions
532            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        // Add a basic document for testing
546        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            // Find a section completion
553            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        // Test non-existent section
602        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        // Test existing detailed documentation
611        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        // Test case insensitivity
618        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        // Test non-existent documentation
623        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        // Add a basic document for testing
635        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        // Test document with sections
665        let document_text = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
666        parser.update_document(&uri, document_text);
667
668        // Test completion at global level (line 0, before any sections)
669        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            // Should only show section completions at global level
674            assert!(items.iter().any(|item| item.label == "[Unit]"));
675            assert!(items.iter().any(|item| item.label == "[Service]"));
676            // Should not show directives at global level
677            assert!(!items.iter().any(|item| item.label == "Description"));
678            assert!(!items.iter().any(|item| item.label == "Type"));
679        }
680
681        // Test completion inside Unit section (line 1)
682        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            // Should only show Unit section directives
687            assert!(items.iter().any(|item| item.label == "Description"));
688            assert!(items.iter().any(|item| item.label == "Documentation"));
689            // Should not show Service-specific directives
690            assert!(!items.iter().any(|item| item.label == "Type"));
691            assert!(!items.iter().any(|item| item.label == "ExecStart"));
692        }
693
694        // Test completion inside Service section (line 4)
695        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            // Should only show Service section directives
700            assert!(items.iter().any(|item| item.label == "Type"));
701            assert!(items.iter().any(|item| item.label == "ExecStart"));
702            // Should not show Unit-specific directives
703            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        // Test document with partial section header
714        let document_text = "[Un";
715        parser.update_document(&uri, document_text);
716
717        // Test completion in the middle of a section header
718        let result = completion
719            .get_completions(&parser, &uri, &Position::new(0, 3))
720            .await;
721        if let Some(CompletionResponse::Array(items)) = result {
722            // Should show section completions when in a section header
723            assert!(items.iter().any(|item| item.label == "[Unit]"));
724            assert!(items.iter().any(|item| item.label == "[Service]"));
725            // Should not show directives in section header context
726            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}