use log::debug;
use std::path::PathBuf;
use tower_lsp_server::lsp_types::{GotoDefinitionResponse, Location, Position, Range, Uri};
use tower_lsp_server::UriExt;
use crate::constants::SystemdConstants;
use crate::parser::SystemdParser;
#[derive(Debug)]
pub struct SystemdDefinitionProvider {
shared_temp_file: Option<PathBuf>,
}
impl SystemdDefinitionProvider {
pub fn new() -> Self {
let shared_temp_file = if let Ok(temp_dir) = std::env::temp_dir().canonicalize() {
let temp_file = temp_dir.join("systemdls-documentation.md");
if !temp_file.exists() {
let initial_content = "# systemd Documentation\n\nSelect a section header and use goto definition to view documentation.\n";
if std::fs::write(&temp_file, initial_content).is_ok() {
debug!("Created shared temp file for documentation");
Some(temp_file)
} else {
debug!("Failed to create shared temp file");
None
}
} else {
debug!("Reusing existing shared temp file");
Some(temp_file)
}
} else {
debug!("Failed to get temp directory");
None
};
Self { shared_temp_file }
}
pub async fn get_definition(
&self,
parser: &SystemdParser,
uri: &Uri,
position: &Position,
) -> Option<GotoDefinitionResponse> {
debug!(
"Definition request at {}:{} in {:?}",
position.line, position.character, uri
);
let parsed = parser.get_parsed_document(uri)?;
debug!(
"Found parsed document with {} sections",
parsed.sections.len()
);
for (name, section) in &parsed.sections {
debug!(
"Section '{}' at lines {}-{}",
name, section.line_range.0, section.line_range.1
);
}
if let Some(section_name) = parser.get_section_header_at_position(&parsed, position) {
debug!("Found section header '{}' at position", section_name);
return self.get_section_man_page_definition(§ion_name).await;
} else {
debug!(
"No section header found at position {}:{}",
position.line, position.character
);
}
None
}
async fn get_section_man_page_definition(
&self,
section_name: &str,
) -> Option<GotoDefinitionResponse> {
let docs = SystemdConstants::section_documentation();
let content = docs.iter().find_map(|(key, value)| {
if key.eq_ignore_ascii_case(section_name) {
Some(*value)
} else {
None
}
});
if let Some(temp_file) = &self.shared_temp_file {
if let Some(content) = content {
if std::fs::write(temp_file, content).is_ok() {
debug!(
"Updated shared temp file with {} documentation",
section_name
);
if let Some(uri) = Uri::from_file_path(temp_file) {
let location = Location {
uri,
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 0,
},
},
};
return Some(GotoDefinitionResponse::Scalar(location));
}
}
}
}
debug!("No documentation available for section: {}", section_name);
None
}
pub fn get_embedded_documentation(&self, section_key: &str) -> Option<String> {
let docs = SystemdConstants::section_documentation();
docs.iter().find_map(|(key, value)| {
if key.eq_ignore_ascii_case(section_key) {
Some(value.to_string())
} else {
None
}
})
}
pub fn cleanup_temp_files(&self) {
if let Some(temp_file) = &self.shared_temp_file {
if temp_file.exists() {
if let Err(e) = std::fs::remove_file(temp_file) {
debug!("Failed to remove shared temp file: {}", e);
} else {
debug!("Cleaned up shared temp file");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::SystemdParser;
use tower_lsp_server::lsp_types::{Position, Uri};
#[test]
fn test_embedded_documentation_exists() {
let docs = SystemdConstants::section_documentation();
let expected_sections = vec![
"Unit", "Service", "Install", "Socket", "Timer",
"Mount", "Path", "Swap", "Automount",
"Slice", "Scope"
];
for section in expected_sections {
assert!(docs.contains_key(section), "{} should exist", section);
assert!(!docs[section].is_empty(), "{} docs should not be empty", section);
}
}
#[tokio::test]
async fn test_get_definition_for_valid_section() {
let provider = SystemdDefinitionProvider::new();
let parser = SystemdParser::new();
let content = "[Unit]\nDescription=Test service\n\n[Service]\nType=simple\n";
let _parsed = parser.parse(content);
let uri = "file:///test.service".parse::<Uri>().unwrap();
parser.update_document(&uri, content);
let position = Position {
line: 0,
character: 0,
};
let result = provider.get_definition(&parser, &uri, &position).await;
assert!(result.is_some());
if let Some(GotoDefinitionResponse::Scalar(location)) = result {
assert!(location
.uri
.to_string()
.contains("systemdls-documentation.md"));
}
}
#[tokio::test]
async fn test_get_definition_for_invalid_position() {
let provider = SystemdDefinitionProvider::new();
let parser = SystemdParser::new();
let content = "[Unit]\nDescription=Test service\n";
let uri = "file:///test.service".parse::<Uri>().unwrap();
parser.update_document(&uri, content);
let position = Position {
line: 1,
character: 0,
};
let result = provider.get_definition(&parser, &uri, &position).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_get_definition_for_unknown_section() {
let provider = SystemdDefinitionProvider::new();
let parser = SystemdParser::new();
let content = "[Unknown]\nSomeDirective=value\n";
let uri = "file:///test.service".parse::<Uri>().unwrap();
parser.update_document(&uri, content);
let position = Position {
line: 0,
character: 0,
};
let result = provider.get_definition(&parser, &uri, &position).await;
assert!(result.is_none());
}
#[tokio::test]
async fn test_get_definition_case_insensitive() {
let provider = SystemdDefinitionProvider::new();
let parser = SystemdParser::new();
let test_cases = ["[UNIT]", "[Unit]", "[unit]"];
for (i, section_header) in test_cases.iter().enumerate() {
let content = format!("{}\nDescription=Test\n", section_header);
let uri = format!("file:///test_{}.service", i)
.parse::<Uri>()
.unwrap();
parser.update_document(&uri, &content);
let position = Position {
line: 0,
character: 0,
};
let result = provider.get_definition(&parser, &uri, &position).await;
assert!(
result.is_some(),
"Failed for section header: {}",
section_header
);
}
}
#[test]
fn test_documentation_content_quality() {
let docs = SystemdConstants::section_documentation();
let unit_docs = docs["Unit"];
assert!(unit_docs.len() > 100, "Unit docs should be substantial");
assert!(unit_docs.contains("[Unit]"));
let service_docs = docs["Service"];
assert!(service_docs.len() > 100, "Service docs should be substantial");
assert!(service_docs.contains("[Service]"));
let install_docs = docs["Install"];
assert!(install_docs.len() > 100, "Install docs should be substantial");
assert!(install_docs.contains("[Install]"));
}
}