1use dashmap::DashMap;
2use log::{debug, trace};
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use tower_lsp_server::lsp_types::{Position, Uri};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SystemdUnit {
10 pub sections: HashMap<String, SystemdSection>,
11 pub raw_text: String,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct SystemdSection {
16 pub name: String,
17 pub directives: Vec<SystemdDirective>,
18 pub line_range: (u32, u32),
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DirectiveValueSpan {
23 pub line: u32,
24 pub start: u32,
25 pub end: u32,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct SystemdDirective {
30 pub key: String,
31 pub value: String,
32 pub line_number: u32,
33 pub column_range: (u32, u32),
34 pub end_line_number: u32,
35 pub value_spans: Vec<DirectiveValueSpan>,
36}
37
38#[derive(Debug)]
39pub struct SystemdParser {
40 documents: DashMap<Uri, SystemdUnit>,
41 section_regex: Regex,
42 directive_regex: Regex,
43}
44
45impl SystemdParser {
46 pub fn new() -> Self {
47 Self {
48 documents: DashMap::new(),
49 section_regex: Regex::new(r"^\[([^\]]+)\]$").unwrap(),
50 directive_regex: Regex::new(r"^([^=]+)=(.*)$").unwrap(),
51 }
52 }
53
54 pub fn parse(&self, text: &str) -> SystemdUnit {
55 trace!("Parsing systemd unit file ({} characters)", text.len());
56 let mut unit = SystemdUnit {
57 sections: HashMap::new(),
58 raw_text: text.to_string(),
59 };
60
61 let mut current_section: Option<String> = None;
62
63 let mut lines = text.lines().enumerate().peekable();
64
65 while let Some((raw_line_num, line)) = lines.next() {
66 let line_num = raw_line_num as u32;
67 let trimmed = line.trim();
68
69 if trimmed.is_empty() || trimmed.starts_with('#') {
70 continue;
71 }
72
73 if let Some(captures) = self.section_regex.captures(trimmed) {
74 if let Some(section_name) = current_section.take() {
75 if let Some(section) = unit.sections.get_mut(§ion_name) {
76 section.line_range.1 = line_num - 1;
77 }
78 }
79
80 let section_name = captures[1].to_string();
81 current_section = Some(section_name.clone());
82
83 unit.sections.insert(
84 section_name.clone(),
85 SystemdSection {
86 name: section_name,
87 directives: Vec::new(),
88 line_range: (line_num, line_num),
89 },
90 );
91 } else if let Some(captures) = self.directive_regex.captures(trimmed) {
92 if let Some(section_name) = ¤t_section {
93 let key = captures[1].trim().to_string();
94 let raw_value = captures[2].to_string();
95
96 let key_start = line.find(&key).unwrap_or(0) as u32;
97 let key_end = key_start + key.len() as u32;
98
99 let eq_index = line.find('=').map(|idx| idx as u32);
100 let mut value_start = eq_index.unwrap_or(key_end) + 1;
101 let after_eq = if let Some(eq_idx) = line.find('=') {
102 &line[eq_idx + 1..]
103 } else {
104 ""
105 };
106 let leading_ws = after_eq.chars().take_while(|c| c.is_whitespace()).count();
107 value_start += leading_ws as u32;
108
109 let (mut fragment, mut continuation) = parse_value_fragment(&raw_value);
110 let mut normalized_value = fragment.clone();
111 let mut value_spans = Vec::new();
112
113 let first_span_end = value_start + fragment.len() as u32;
114 value_spans.push(DirectiveValueSpan {
115 line: line_num,
116 start: value_start,
117 end: first_span_end,
118 });
119
120 let mut end_line_number = line_num;
121
122 while continuation {
123 if let Some((next_line_num, next_line)) = lines.next() {
124 let next_line_trimmed = next_line.trim();
125 end_line_number = next_line_num as u32;
126
127 let indent = next_line.find(next_line_trimmed).unwrap_or(0) as u32;
128
129 let (next_fragment, next_continuation) =
130 parse_value_fragment(next_line_trimmed);
131
132 if !next_fragment.is_empty() {
133 if normalized_value.is_empty() {
134 normalized_value = next_fragment.clone();
135 } else {
136 normalized_value.push(' ');
137 normalized_value.push_str(&next_fragment);
138 }
139 }
140
141 value_spans.push(DirectiveValueSpan {
142 line: end_line_number,
143 start: indent,
144 end: indent + next_fragment.len() as u32,
145 });
146
147 continuation = next_continuation;
148 fragment = next_fragment;
149 } else {
150 break;
151 }
152 }
153
154 if normalized_value.is_empty() {
155 normalized_value = fragment;
156 }
157
158 if normalized_value.is_empty() {
160 value_spans.clear();
161 value_spans.push(DirectiveValueSpan {
162 line: line_num,
163 start: value_start,
164 end: value_start,
165 });
166 end_line_number = line_num;
167 } else if value_spans.is_empty() {
168 value_spans.push(DirectiveValueSpan {
169 line: line_num,
170 start: value_start,
171 end: value_start + normalized_value.len() as u32,
172 });
173 }
174
175 let directive = SystemdDirective {
176 key: key.clone(),
177 value: normalized_value,
178 line_number: line_num,
179 column_range: (key_start, key_end),
180 end_line_number,
181 value_spans,
182 };
183
184 if let Some(section) = unit.sections.get_mut(section_name) {
185 section.directives.push(directive);
186 }
187 }
188 }
189 }
190
191 if let Some(section_name) = current_section {
192 if let Some(section) = unit.sections.get_mut(§ion_name) {
193 section.line_range.1 = text.lines().count() as u32;
194 }
195 }
196
197 debug!(
198 "Parsed {} sections with {} total directives",
199 unit.sections.len(),
200 unit.sections
201 .values()
202 .map(|s| s.directives.len())
203 .sum::<usize>()
204 );
205 unit
206 }
207
208 pub fn update_document(&self, uri: &Uri, text: &str) {
209 let parsed = self.parse(text);
210 self.documents.insert(uri.clone(), parsed);
211 }
212
213 pub fn get_parsed_document(&self, uri: &Uri) -> Option<SystemdUnit> {
214 self.documents.get(uri).map(|entry| entry.clone())
215 }
216
217 pub fn get_document_text(&self, uri: &Uri) -> Option<String> {
218 self.documents.get(uri).map(|entry| entry.raw_text.clone())
219 }
220
221 pub fn get_word_at_position(&self, unit: &SystemdUnit, position: &Position) -> Option<String> {
222 let lines: Vec<&str> = unit.raw_text.lines().collect();
223 if let Some(line) = lines.get(position.line as usize) {
224 let chars: Vec<char> = line.chars().collect();
226 if position.character < chars.len() as u32 {
227 let cursor_pos = position.character as usize;
228
229 let mut start = cursor_pos;
231 let mut end = cursor_pos;
232
233 while start > 0
235 && (chars[start - 1].is_alphanumeric()
236 || chars[start - 1] == '-'
237 || chars[start - 1] == '_'
238 || chars[start - 1] == '.')
239 {
240 start -= 1;
241 }
242
243 while end < chars.len()
245 && (chars[end].is_alphanumeric()
246 || chars[end] == '-'
247 || chars[end] == '_'
248 || chars[end] == '.')
249 {
250 end += 1;
251 }
252
253 if start < end {
254 return Some(chars[start..end].iter().collect());
255 }
256 }
257 }
258 None
259 }
260
261 pub fn get_section_header_at_position(
262 &self,
263 unit: &SystemdUnit,
264 position: &Position,
265 ) -> Option<String> {
266 debug!("Checking for section header at line {}", position.line);
267 for section in unit.sections.values() {
268 if position.line == section.line_range.0 {
269 debug!(
270 "Found section header '{}' at line {}",
271 section.name, position.line
272 );
273 return Some(section.name.clone());
274 }
275 }
276 debug!("No section header found at line {}", position.line);
277 None
278 }
279
280 pub fn get_section_at_line<'a>(
281 &self,
282 unit: &'a SystemdUnit,
283 line: u32,
284 ) -> Option<&'a SystemdSection> {
285 unit.sections
286 .values()
287 .find(|section| line >= section.line_range.0 && line <= section.line_range.1)
288 }
289}
290
291fn parse_value_fragment(text: &str) -> (String, bool) {
292 let trimmed = text.trim();
293 if trimmed.is_empty() {
294 return (String::new(), false);
295 }
296
297 let mut backslash_count = 0usize;
298 for ch in trimmed.chars().rev() {
299 if ch == '\\' {
300 backslash_count += 1;
301 } else {
302 break;
303 }
304 }
305
306 let continuation = backslash_count % 2 == 1;
307 let mut fragment = trimmed.to_string();
308
309 if continuation {
310 fragment.pop();
311 fragment = fragment.trim_end().to_string();
312 }
313
314 (fragment, continuation)
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use tower_lsp_server::lsp_types::{Position, Uri};
321
322 #[test]
323 fn test_parse_basic_systemd_file() {
324 let parser = SystemdParser::new();
325 let content = "[Unit]\nDescription=Test service\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/bin/test\n";
326
327 let parsed = parser.parse(content);
328
329 assert_eq!(parsed.sections.len(), 2);
330 assert!(parsed.sections.contains_key("Unit"));
331 assert!(parsed.sections.contains_key("Service"));
332
333 let unit_section = &parsed.sections["Unit"];
334 assert_eq!(unit_section.line_range.0, 0);
335 assert_eq!(unit_section.directives.len(), 2);
336 assert!(unit_section
337 .directives
338 .iter()
339 .find(|directive| directive.key == "Description")
340 .is_some());
341 assert!(unit_section
342 .directives
343 .iter()
344 .find(|directive| directive.key == "After")
345 .is_some());
346
347 let service_section = &parsed.sections["Service"];
348 assert_eq!(service_section.line_range.0, 4);
349 assert_eq!(service_section.directives.len(), 2);
350 assert!(service_section
351 .directives
352 .iter()
353 .find(|directive| directive.key == "Type")
354 .is_some());
355 assert!(service_section
356 .directives
357 .iter()
358 .find(|directive| directive.key == "ExecStart")
359 .is_some());
360 }
361
362 #[test]
363 fn test_parse_with_comments_and_empty_lines() {
364 let parser = SystemdParser::new();
365 let content = "# This is a comment\n\n[Unit]\n# Another comment\nDescription=Test\n\n[Service]\nType=simple\n";
366
367 let parsed = parser.parse(content);
368
369 assert_eq!(parsed.sections.len(), 2);
370 assert!(parsed.sections.contains_key("Unit"));
371 assert!(parsed.sections.contains_key("Service"));
372
373 let unit_section = &parsed.sections["Unit"];
375 assert_eq!(unit_section.directives.len(), 1);
376 assert!(unit_section
377 .directives
378 .iter()
379 .find(|directive| directive.key == "Description")
380 .is_some());
381 }
382
383 #[test]
384 fn test_get_section_header_at_position() {
385 let parser = SystemdParser::new();
386 let content = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n\n[Install]\nWantedBy=multi-user.target\n";
387 let parsed = parser.parse(content);
388
389 assert_eq!(
391 parser.get_section_header_at_position(
392 &parsed,
393 &Position {
394 line: 0,
395 character: 0
396 }
397 ),
398 Some("Unit".to_string())
399 );
400 assert_eq!(
401 parser.get_section_header_at_position(
402 &parsed,
403 &Position {
404 line: 3,
405 character: 0
406 }
407 ),
408 Some("Service".to_string())
409 );
410 assert_eq!(
411 parser.get_section_header_at_position(
412 &parsed,
413 &Position {
414 line: 6,
415 character: 0
416 }
417 ),
418 Some("Install".to_string())
419 );
420
421 assert_eq!(
423 parser.get_section_header_at_position(
424 &parsed,
425 &Position {
426 line: 1,
427 character: 0
428 }
429 ),
430 None
431 );
432 assert_eq!(
433 parser.get_section_header_at_position(
434 &parsed,
435 &Position {
436 line: 4,
437 character: 0
438 }
439 ),
440 None
441 );
442 assert_eq!(
443 parser.get_section_header_at_position(
444 &parsed,
445 &Position {
446 line: 7,
447 character: 0
448 }
449 ),
450 None
451 );
452 }
453
454 #[test]
455 fn test_get_section_at_line() {
456 let parser = SystemdParser::new();
457 let content = "[Unit]\nDescription=Test\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/bin/test\n";
458 let parsed = parser.parse(content);
459
460 let unit_section = parser.get_section_at_line(&parsed, 0).unwrap();
462 assert_eq!(unit_section.name, "Unit");
463
464 let unit_section = parser.get_section_at_line(&parsed, 1).unwrap();
465 assert_eq!(unit_section.name, "Unit");
466
467 let unit_section = parser.get_section_at_line(&parsed, 2).unwrap();
468 assert_eq!(unit_section.name, "Unit");
469
470 let service_section = parser.get_section_at_line(&parsed, 4).unwrap();
471 assert_eq!(service_section.name, "Service");
472
473 let service_section = parser.get_section_at_line(&parsed, 5).unwrap();
474 assert_eq!(service_section.name, "Service");
475
476 assert!(parser.get_section_at_line(&parsed, 100).is_none());
480 }
481
482 #[test]
483 fn test_get_word_at_position() {
484 let parser = SystemdParser::new();
485 let content = "[Unit]\nDescription=Test service\nAfter=network.target\n";
486 let parsed = parser.parse(content);
487
488 assert_eq!(
490 parser.get_word_at_position(
491 &parsed,
492 &Position {
493 line: 1,
494 character: 0
495 }
496 ),
497 Some("Description".to_string())
498 );
499 assert_eq!(
500 parser.get_word_at_position(
501 &parsed,
502 &Position {
503 line: 1,
504 character: 5
505 }
506 ),
507 Some("Description".to_string())
508 );
509 assert_eq!(
510 parser.get_word_at_position(
511 &parsed,
512 &Position {
513 line: 2,
514 character: 0
515 }
516 ),
517 Some("After".to_string())
518 );
519
520 assert_eq!(
522 parser.get_word_at_position(
523 &parsed,
524 &Position {
525 line: 1,
526 character: 12
527 }
528 ),
529 Some("Test".to_string())
530 );
531 assert_eq!(
532 parser.get_word_at_position(
533 &parsed,
534 &Position {
535 line: 2,
536 character: 6
537 }
538 ),
539 Some("network.target".to_string())
540 );
541
542 assert_eq!(
544 parser.get_word_at_position(
545 &parsed,
546 &Position {
547 line: 2,
548 character: 10
549 }
550 ),
551 Some("network.target".to_string())
552 );
553 assert_eq!(
554 parser.get_word_at_position(
555 &parsed,
556 &Position {
557 line: 2,
558 character: 14
559 }
560 ),
561 Some("network.target".to_string())
562 );
563 }
564
565 #[test]
566 fn test_document_storage_and_retrieval() {
567 let parser = SystemdParser::new();
568 let content = "[Unit]\nDescription=Test\n";
569 let uri = "file:///test.service".parse::<Uri>().unwrap();
570
571 assert!(parser.get_parsed_document(&uri).is_none());
573 assert!(parser.get_document_text(&uri).is_none());
574
575 parser.update_document(&uri, content);
577
578 let retrieved = parser.get_parsed_document(&uri).unwrap();
580 assert_eq!(retrieved.sections.len(), 1);
581 assert!(retrieved.sections.contains_key("Unit"));
582
583 let text = parser.get_document_text(&uri).unwrap();
584 assert_eq!(text, content);
585 }
586
587 #[test]
588 fn test_parse_edge_cases() {
589 let parser = SystemdParser::new();
590
591 let empty_parsed = parser.parse("");
593 assert_eq!(empty_parsed.sections.len(), 0);
594
595 let comments_only = parser.parse("# Comment 1\n# Comment 2\n");
597 assert_eq!(comments_only.sections.len(), 0);
598
599 let empty_section = parser.parse("[Unit]\n\n[Service]\n");
601 assert_eq!(empty_section.sections.len(), 2);
602 assert_eq!(empty_section.sections["Unit"].directives.len(), 0);
603 assert_eq!(empty_section.sections["Service"].directives.len(), 0);
604
605 let empty_value = parser.parse("[Unit]\nDescription=\n");
607 assert_eq!(empty_value.sections.len(), 1);
608 assert_eq!(
609 empty_value.sections["Unit"]
610 .directives
611 .iter()
612 .find(|directive| directive.key == "Description")
613 .unwrap()
614 .value,
615 ""
616 );
617
618 let spaced_equals = parser.parse("[Unit]\nDescription = Test Service \n");
620 assert_eq!(spaced_equals.sections.len(), 1);
621 assert_eq!(
622 spaced_equals.sections["Unit"]
623 .directives
624 .iter()
625 .find(|directive| directive.key == "Description")
626 .unwrap()
627 .value,
628 "Test Service"
629 );
630 }
631
632 #[test]
633 fn test_case_sensitivity() {
634 let parser = SystemdParser::new();
635 let content = "[UNIT]\nDESCRIPTION=Test\n[service]\ntype=simple\n";
636 let parsed = parser.parse(content);
637
638 assert!(parsed.sections.contains_key("UNIT"));
640 assert!(parsed.sections.contains_key("service"));
641 assert!(!parsed.sections.contains_key("Unit"));
642 assert!(!parsed.sections.contains_key("Service"));
643
644 assert!(parsed.sections["UNIT"]
646 .directives
647 .iter()
648 .find(|directive| directive.key == "DESCRIPTION")
649 .is_some());
650 assert!(parsed.sections["service"]
651 .directives
652 .iter()
653 .find(|directive| directive.key == "type")
654 .is_some());
655 }
656
657 #[test]
658 fn test_parse_multiline_directive_execstart() {
659 let parser = SystemdParser::new();
660 let content =
661 "[Service]\nExecStart=/usr/bin/test \\\n --flag value \\\n --another-flag\n";
662
663 let parsed = parser.parse(content);
664 let service_section = parsed
665 .sections
666 .get("Service")
667 .expect("Service section missing");
668 let exec_start = service_section
669 .directives
670 .iter()
671 .find(|directive| directive.key == "ExecStart")
672 .expect("ExecStart directive missing");
673
674 assert_eq!(
675 exec_start.value,
676 "/usr/bin/test --flag value --another-flag"
677 );
678 assert_eq!(exec_start.line_number, 1);
679 assert_eq!(exec_start.end_line_number, 3);
680 assert_eq!(exec_start.value_spans.len(), 3);
681
682 let first_span = &exec_start.value_spans[0];
683 assert_eq!(first_span.line, 1);
684 assert_eq!(first_span.start, 10);
685 assert_eq!(first_span.end, 23);
686
687 let second_span = &exec_start.value_spans[1];
688 assert_eq!(second_span.line, 2);
689 assert_eq!(second_span.start, 4);
690 assert_eq!(second_span.end, 16);
691
692 let third_span = &exec_start.value_spans[2];
693 assert_eq!(third_span.line, 3);
694 assert_eq!(third_span.start, 4);
695 assert_eq!(third_span.end, 18);
696 }
697}