systemd_lsp/
definition.rs1use 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)]
10pub struct SystemdDefinitionProvider {
12 shared_temp_file: Option<PathBuf>,
13}
14
15impl SystemdDefinitionProvider {
16 pub fn new() -> Self {
17 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 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 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(§ion_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 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 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 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 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 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 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 parser.update_document(&uri, content);
189
190 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 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 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 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 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}