use crate::loader::PluginScripts;
use std::path::{Path, PathBuf};
#[derive(Debug, PartialEq, Eq)]
pub struct HelptagsReport {
pub target_count: usize,
pub ran: bool,
pub exit_code: Option<i32>,
}
pub fn collect_helptag_targets(
plugin_scripts: &[PluginScripts],
merged_dir: &Path,
) -> Vec<PathBuf> {
let mut targets = Vec::new();
let merged_doc = merged_dir.join("doc");
if merged_doc.is_dir() {
targets.push(merged_doc);
}
for ps in plugin_scripts {
if ps.merge && !ps.lazy {
continue;
}
let doc = PathBuf::from(&ps.path).join("doc");
if doc.is_dir() {
targets.push(doc);
}
}
targets
}
pub fn build_helptags_script(targets: &[PathBuf]) -> String {
let mut script = String::new();
for t in targets {
let p = t.to_string_lossy().replace('\\', "/").replace('\'', "''");
script.push_str(&format!(
"try | execute 'helptags' fnameescape('{}') | catch | echomsg 'rvpm helptags: ' . v:exception | endtry\n",
p
));
}
script
}
pub async fn build_helptags(
plugin_scripts: &[PluginScripts],
merged_dir: &Path,
) -> anyhow::Result<HelptagsReport> {
let targets = collect_helptag_targets(plugin_scripts, merged_dir);
if targets.is_empty() {
return Ok(HelptagsReport {
target_count: 0,
ran: false,
exit_code: None,
});
}
let script = build_helptags_script(&targets);
let tmp_path = std::env::temp_dir().join(format!(
"rvpm-helptags-{}-{}.vim",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::write(&tmp_path, &script)?;
let escaped_path = tmp_path
.display()
.to_string()
.replace('\\', "/")
.replace('\'', "''");
let source_arg = format!("execute 'source ' . fnameescape('{}')", escaped_path);
let result = tokio::process::Command::new("nvim")
.args(["--headless", "--clean", "-c", &source_arg, "-c", "qa!"])
.output()
.await;
let _ = std::fs::remove_file(&tmp_path);
match result {
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stderr_trim = stderr.trim();
if !stderr_trim.is_empty() {
eprintln!("{}", stderr_trim);
}
let exit_code = out.status.code();
Ok(HelptagsReport {
target_count: targets.len(),
ran: true,
exit_code,
})
}
Err(e) => {
eprintln!(
"\u{26a0} helptags skipped: could not run `nvim --headless` ({})",
e
);
Ok(HelptagsReport {
target_count: targets.len(),
ran: false,
exit_code: None,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_plugin(name: &str, path: &Path, lazy: bool, merge: bool) -> PluginScripts {
let mut s = PluginScripts::for_test(name, path.to_str().unwrap());
s.lazy = lazy;
s.merge = merge;
s
}
fn create_doc(root: &Path, plugin: &str) -> PathBuf {
let plugin_dir = root.join(plugin);
let doc = plugin_dir.join("doc");
std::fs::create_dir_all(&doc).unwrap();
std::fs::write(doc.join("foo.txt"), b"*foo-tag*\n").unwrap();
plugin_dir
}
#[test]
fn test_collect_targets_includes_merged_doc_when_exists() {
let tmp = TempDir::new().unwrap();
let merged = tmp.path().join("merged");
std::fs::create_dir_all(merged.join("doc")).unwrap();
let targets = collect_helptag_targets(&[], &merged);
assert_eq!(targets, vec![merged.join("doc")]);
}
#[test]
fn test_collect_targets_skips_merged_when_doc_missing() {
let tmp = TempDir::new().unwrap();
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let targets = collect_helptag_targets(&[], &merged);
assert!(targets.is_empty());
}
#[test]
fn test_collect_targets_skips_eager_merge_plugin() {
let tmp = TempDir::new().unwrap();
let plugin_dir = create_doc(tmp.path(), "plug1");
let ps = make_plugin("plug1", &plugin_dir, false, true);
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let targets = collect_helptag_targets(&[ps], &merged);
assert!(targets.is_empty());
}
#[test]
fn test_collect_targets_includes_lazy_plugin_even_when_merge_true() {
let tmp = TempDir::new().unwrap();
let plugin_dir = create_doc(tmp.path(), "plug2");
let ps = make_plugin("plug2", &plugin_dir, true, true);
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let targets = collect_helptag_targets(&[ps], &merged);
assert_eq!(targets, vec![plugin_dir.join("doc")]);
}
#[test]
fn test_collect_targets_includes_eager_non_merge_plugin() {
let tmp = TempDir::new().unwrap();
let plugin_dir = create_doc(tmp.path(), "plug3");
let ps = make_plugin("plug3", &plugin_dir, false, false);
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let targets = collect_helptag_targets(&[ps], &merged);
assert_eq!(targets, vec![plugin_dir.join("doc")]);
}
#[test]
fn test_collect_targets_skips_plugin_without_doc() {
let tmp = TempDir::new().unwrap();
let plugin_dir = tmp.path().join("no_doc");
std::fs::create_dir_all(&plugin_dir).unwrap();
let ps = make_plugin("no_doc", &plugin_dir, true, false);
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let targets = collect_helptag_targets(&[ps], &merged);
assert!(targets.is_empty());
}
#[test]
fn test_collect_targets_orders_merged_first_then_per_plugin() {
let tmp = TempDir::new().unwrap();
std::fs::create_dir_all(tmp.path().join("merged").join("doc")).unwrap();
let lazy_plugin = create_doc(tmp.path(), "lazy_p");
let non_merge = create_doc(tmp.path(), "non_merge");
let scripts = vec![
make_plugin("lazy_p", &lazy_plugin, true, true),
make_plugin("non_merge", &non_merge, false, false),
];
let merged = tmp.path().join("merged");
let targets = collect_helptag_targets(&scripts, &merged);
assert_eq!(
targets,
vec![
merged.join("doc"),
lazy_plugin.join("doc"),
non_merge.join("doc"),
]
);
}
#[test]
fn test_build_script_uses_fnameescape_and_try_catch() {
let path = PathBuf::from("/tmp/some plugin/doc");
let script = build_helptags_script(&[path]);
assert!(script.contains("fnameescape('/tmp/some plugin/doc')"));
assert!(script.contains("try |"));
assert!(script.contains("| endtry"));
}
#[test]
fn test_build_script_escapes_single_quote_in_path() {
let path = PathBuf::from("/tmp/it's/doc");
let script = build_helptags_script(&[path]);
assert!(script.contains("'/tmp/it''s/doc'"));
}
#[test]
fn test_build_script_normalizes_backslashes_to_forward() {
let path = PathBuf::from(r"C:\Users\foo\plugin\doc");
let script = build_helptags_script(&[path]);
assert!(script.contains("'C:/Users/foo/plugin/doc'"));
assert!(!script.contains(r"\Users"));
}
#[tokio::test]
async fn test_build_helptags_skips_when_no_targets() {
let tmp = TempDir::new().unwrap();
let merged = tmp.path().join("merged");
std::fs::create_dir_all(&merged).unwrap();
let report = build_helptags(&[], &merged).await.unwrap();
assert_eq!(report.target_count, 0);
assert!(!report.ran);
}
}