systemd_lsp/
definition.rs

1use log::debug;
2use std::path::PathBuf;
3use tower_lsp_server::lsp_types::{GotoDefinitionResponse, Location, Position, Range, Uri};
4use tower_lsp_server::UriExt;
5
6use crate::constants::SystemdConstants;
7use crate::parser::SystemdParser;
8
9#[derive(Debug)]
10// this is the shared file for loading the embedded documentation
11pub struct SystemdDefinitionProvider {
12    shared_temp_file: Option<PathBuf>,
13}
14
15impl SystemdDefinitionProvider {
16    pub fn new() -> Self {
17        // Create a single shared temp file for all documentation
18        let shared_temp_file = if let Ok(temp_dir) = std::env::temp_dir().canonicalize() {
19            let temp_file = temp_dir.join("systemdls-documentation.md");
20
21            // Create the file with initial content if it doesn't exist
22            if !temp_file.exists() {
23                let initial_content = "# systemd Documentation\n\nSelect a section header and use goto definition to view documentation.\n";
24                if std::fs::write(&temp_file, initial_content).is_ok() {
25                    debug!("Created shared temp file for documentation");
26                    Some(temp_file)
27                } else {
28                    debug!("Failed to create shared temp file");
29                    None
30                }
31            } else {
32                debug!("Reusing existing shared temp file");
33                Some(temp_file)
34            }
35        } else {
36            debug!("Failed to get temp directory");
37            None
38        };
39
40        Self { shared_temp_file }
41    }
42
43    pub async fn get_definition(
44        &self,
45        parser: &SystemdParser,
46        uri: &Uri,
47        position: &Position,
48    ) -> Option<GotoDefinitionResponse> {
49        debug!(
50            "Definition request at {}:{} in {:?}",
51            position.line, position.character, uri
52        );
53
54        let parsed = parser.get_parsed_document(uri)?;
55
56        debug!(
57            "Found parsed document with {} sections",
58            parsed.sections.len()
59        );
60        for (name, section) in &parsed.sections {
61            debug!(
62                "Section '{}' at lines {}-{}",
63                name, section.line_range.0, section.line_range.1
64            );
65        }
66
67        // Only handle section headers for go-to definition
68        if let Some(section_name) = parser.get_section_header_at_position(&parsed, position) {
69            debug!("Found section header '{}' at position", section_name);
70            return self.get_section_man_page_definition(&section_name).await;
71        } else {
72            debug!(
73                "No section header found at position {}:{}",
74                position.line, position.character
75            );
76        }
77
78        None
79    }
80
81    async fn get_section_man_page_definition(
82        &self,
83        section_name: &str,
84    ) -> Option<GotoDefinitionResponse> {
85        let docs = SystemdConstants::section_documentation();
86
87        // Try to find documentation (case-insensitive)
88        let content = docs.iter().find_map(|(key, value)| {
89            if key.eq_ignore_ascii_case(section_name) {
90                Some(*value)
91            } else {
92                None
93            }
94        });
95
96        // Update the shared temp file with the requested section's documentation
97        if let Some(temp_file) = &self.shared_temp_file {
98            if let Some(content) = content {
99                if std::fs::write(temp_file, content).is_ok() {
100                    debug!(
101                        "Updated shared temp file with {} documentation",
102                        section_name
103                    );
104                    if let Some(uri) = Uri::from_file_path(temp_file) {
105                        let location = Location {
106                            uri,
107                            range: Range {
108                                start: Position {
109                                    line: 0,
110                                    character: 0,
111                                },
112                                end: Position {
113                                    line: 0,
114                                    character: 0,
115                                },
116                            },
117                        };
118                        return Some(GotoDefinitionResponse::Scalar(location));
119                    }
120                }
121            }
122        }
123
124        debug!("No documentation available for section: {}", section_name);
125        None
126    }
127
128    /// Get embedded documentation for a section
129    pub fn get_embedded_documentation(&self, section_key: &str) -> Option<String> {
130        let docs = SystemdConstants::section_documentation();
131        docs.iter().find_map(|(key, value)| {
132            if key.eq_ignore_ascii_case(section_key) {
133                Some(value.to_string())
134            } else {
135                None
136            }
137        })
138    }
139
140    /// Clean up temporary documentation files
141    pub fn cleanup_temp_files(&self) {
142        if let Some(temp_file) = &self.shared_temp_file {
143            if temp_file.exists() {
144                if let Err(e) = std::fs::remove_file(temp_file) {
145                    debug!("Failed to remove shared temp file: {}", e);
146                } else {
147                    debug!("Cleaned up shared temp file");
148                }
149            }
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::parser::SystemdParser;
158    use tower_lsp_server::lsp_types::{Position, Uri};
159
160    #[test]
161    fn test_embedded_documentation_exists() {
162        let docs = SystemdConstants::section_documentation();
163
164        // Test that all expected documentation is present and not empty
165        let expected_sections = vec![
166            "Unit", "Service", "Install", "Socket", "Timer",
167            "Mount", "Path", "Swap", "Automount",
168            "Slice", "Scope"
169        ];
170
171        for section in expected_sections {
172            assert!(docs.contains_key(section), "{} should exist", section);
173            assert!(!docs[section].is_empty(), "{} docs should not be empty", section);
174        }
175    }
176
177    #[tokio::test]
178    async fn test_get_definition_for_valid_section() {
179        let provider = SystemdDefinitionProvider::new();
180        let parser = SystemdParser::new();
181
182        // Create a test systemd file
183        let content = "[Unit]\nDescription=Test service\n\n[Service]\nType=simple\n";
184        let _parsed = parser.parse(content);
185        let uri = "file:///test.service".parse::<Uri>().unwrap();
186
187        // Store the parsed document
188        parser.update_document(&uri, content);
189
190        // Test definition for Unit section (line 0)
191        let position = Position {
192            line: 0,
193            character: 0,
194        };
195        let result = provider.get_definition(&parser, &uri, &position).await;
196        assert!(result.is_some());
197
198        if let Some(GotoDefinitionResponse::Scalar(location)) = result {
199            assert!(location
200                .uri
201                .to_string()
202                .contains("systemdls-documentation.md"));
203        }
204    }
205
206    #[tokio::test]
207    async fn test_get_definition_for_invalid_position() {
208        let provider = SystemdDefinitionProvider::new();
209        let parser = SystemdParser::new();
210
211        let content = "[Unit]\nDescription=Test service\n";
212        let uri = "file:///test.service".parse::<Uri>().unwrap();
213        parser.update_document(&uri, content);
214
215        // Test position not on a section header (line 1)
216        let position = Position {
217            line: 1,
218            character: 0,
219        };
220        let result = provider.get_definition(&parser, &uri, &position).await;
221        assert!(result.is_none());
222    }
223
224    #[tokio::test]
225    async fn test_get_definition_for_unknown_section() {
226        let provider = SystemdDefinitionProvider::new();
227        let parser = SystemdParser::new();
228
229        // Create a file with an unknown section type
230        let content = "[Unknown]\nSomeDirective=value\n";
231        let uri = "file:///test.service".parse::<Uri>().unwrap();
232        parser.update_document(&uri, content);
233
234        let position = Position {
235            line: 0,
236            character: 0,
237        };
238        let result = provider.get_definition(&parser, &uri, &position).await;
239        assert!(result.is_none());
240    }
241
242    #[tokio::test]
243    async fn test_get_definition_case_insensitive() {
244        let provider = SystemdDefinitionProvider::new();
245        let parser = SystemdParser::new();
246
247        // Test with different case variations
248        let test_cases = ["[UNIT]", "[Unit]", "[unit]"];
249
250        for (i, section_header) in test_cases.iter().enumerate() {
251            let content = format!("{}\nDescription=Test\n", section_header);
252            let uri = format!("file:///test_{}.service", i)
253                .parse::<Uri>()
254                .unwrap();
255            parser.update_document(&uri, &content);
256
257            let position = Position {
258                line: 0,
259                character: 0,
260            };
261            let result = provider.get_definition(&parser, &uri, &position).await;
262            assert!(
263                result.is_some(),
264                "Failed for section header: {}",
265                section_header
266            );
267        }
268    }
269
270    #[test]
271    fn test_documentation_content_quality() {
272        let docs = SystemdConstants::section_documentation();
273
274        // Test that documentation contains useful content
275        let unit_docs = docs["Unit"];
276        assert!(unit_docs.len() > 100, "Unit docs should be substantial");
277        assert!(unit_docs.contains("[Unit]"));
278
279        let service_docs = docs["Service"];
280        assert!(service_docs.len() > 100, "Service docs should be substantial");
281        assert!(service_docs.contains("[Service]"));
282
283        let install_docs = docs["Install"];
284        assert!(install_docs.len() > 100, "Install docs should be substantial");
285        assert!(install_docs.contains("[Install]"));
286    }
287}