use log::{debug, trace};
use tower_lsp_server::lsp_types::{Position, Range, TextEdit, Uri};
#[derive(Debug)]
pub struct SystemdFormatter;
impl SystemdFormatter {
pub fn new() -> Self {
Self
}
pub fn format_document(&self, uri: &Uri, text: &str) -> Vec<TextEdit> {
debug!("Formatting document: {:?}", uri);
trace!("Document text length: {}", text.len());
let formatted_content = self.apply_opinionated_formatting(text);
if formatted_content != text {
vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: text.lines().count() as u32,
character: 0,
},
},
new_text: formatted_content,
}]
} else {
vec![]
}
}
pub fn format_range(&self, uri: &Uri, text: &str, _range: Range) -> Vec<TextEdit> {
debug!("Formatting range in document: {:?}", uri);
self.format_document(uri, text)
}
fn apply_opinionated_formatting(&self, content: &str) -> String {
let lines: Vec<&str> = content.lines().collect();
let mut result = Vec::new();
let mut in_section = false;
let mut previous_was_section = false;
for line in lines.iter() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with('#') {
result.push(trimmed.to_string());
continue;
}
if trimmed.starts_with('[') && trimmed.ends_with(']') {
if in_section || previous_was_section {
result.push(String::new());
}
result.push(trimmed.to_string());
in_section = true;
previous_was_section = true;
continue;
}
if let Some(equals_pos) = trimmed.find('=') {
let key = trimmed[..equals_pos].trim();
let value = trimmed[equals_pos + 1..].trim();
let formatted = format!("{}={}", key, value);
result.push(formatted);
previous_was_section = false;
continue;
}
result.push(trimmed.to_string());
previous_was_section = false;
}
let mut formatted = result.join("\n");
if !formatted.is_empty() && !formatted.ends_with('\n') {
formatted.push('\n');
}
formatted
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_opinionated_formatting_section_spacing() {
let formatter = SystemdFormatter::new();
let input = "[Unit]\nDescription=Test\n[Service]\nType=simple\n[Install]\nWantedBy=multi-user.target\n";
let expected = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n\n[Install]\nWantedBy=multi-user.target\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
#[test]
fn test_opinionated_formatting_no_spaces_around_equals() {
let formatter = SystemdFormatter::new();
let input = "[Unit]\nDescription = Test Service\nAfter = network.target\nWants= network-online.target\n";
let expected =
"[Unit]\nDescription=Test Service\nAfter=network.target\nWants=network-online.target\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
#[test]
fn test_opinionated_formatting_preserves_comments() {
let formatter = SystemdFormatter::new();
let input = "# This is a comment\n[Unit]\n# Another comment\nDescription=Test\n";
let expected = "# This is a comment\n[Unit]\n# Another comment\nDescription=Test\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
#[test]
fn test_opinionated_formatting_removes_extra_blank_lines() {
let formatter = SystemdFormatter::new();
let input = "\n\n[Unit]\n\n\nDescription=Test\n\n\n\n[Service]\n\n\nType=simple\n\n\n";
let expected = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
#[test]
fn test_opinionated_formatting_single_section() {
let formatter = SystemdFormatter::new();
let input = "[Unit]\nDescription=Test\nAfter=network.target\n";
let expected = "[Unit]\nDescription=Test\nAfter=network.target\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
#[test]
fn test_format_document_integration() {
let formatter = SystemdFormatter::new();
let uri = "file:///test.service".parse::<Uri>().unwrap();
let input = "[Unit]\nDescription = Test\n[Service]\nType = simple\n";
let edits = formatter.format_document(&uri, input);
assert_eq!(edits.len(), 1);
let expected = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
assert_eq!(edits[0].new_text, expected);
}
#[test]
fn test_format_document_no_changes_needed() {
let formatter = SystemdFormatter::new();
let uri = "file:///test.service".parse::<Uri>().unwrap();
let input = "[Unit]\nDescription=Test\n\n[Service]\nType=simple\n";
let edits = formatter.format_document(&uri, input);
assert_eq!(edits.len(), 0);
}
#[test]
fn test_opinionated_formatting_no_blank_lines_within_sections() {
let formatter = SystemdFormatter::new();
let input = "[Unit]\nDescription=Test\n\nAfter=network.target\n\n\n[Service]\nType=simple\n\nExecStart=/bin/test\n\nRestart=always\n";
let expected = "[Unit]\nDescription=Test\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/bin/test\nRestart=always\n";
let formatted = formatter.apply_opinionated_formatting(input);
assert_eq!(formatted, expected);
}
}