use crate::config::MapSpec;
use std::path::Path;
#[derive(Clone)]
pub struct PluginScripts {
pub name: String,
pub path: String,
pub merge: bool,
pub init: Option<String>,
pub before: Option<String>,
pub after: Option<String>,
pub plugin_files: Vec<String>,
pub ftdetect_files: Vec<String>,
pub after_plugin_files: Vec<String>,
pub lazy: bool,
pub on_cmd: Option<Vec<String>>,
pub on_ft: Option<Vec<String>>,
pub on_map: Option<Vec<MapSpec>>,
pub on_event: Option<Vec<String>>,
pub on_path: Option<Vec<String>>,
pub on_source: Option<Vec<String>>,
pub depends: Option<Vec<String>>,
pub colorschemes: Vec<String>,
pub cond: Option<String>,
}
impl PluginScripts {
#[cfg(test)]
pub fn for_test(name: &str, path: &str) -> Self {
Self {
name: name.to_string(),
path: path.to_string(),
merge: true,
init: None,
before: None,
after: None,
plugin_files: Vec::new(),
ftdetect_files: Vec::new(),
after_plugin_files: Vec::new(),
lazy: false,
on_cmd: None,
on_ft: None,
on_map: None,
on_event: None,
on_path: None,
on_source: None,
depends: None,
colorschemes: Vec::new(),
cond: None,
}
}
}
fn lua_str_list(items: &[String]) -> String {
if items.is_empty() {
return "{}".to_string();
}
let quoted: Vec<String> = items
.iter()
.map(|s| format!("\"{}\"", s.replace('\\', "/")))
.collect();
format!("{{ {} }}", quoted.join(", "))
}
fn sanitize_name(name: &str) -> String {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
fn push_with_cond(lua: &mut String, cond: &Option<String>, body: &str) {
if let Some(c) = cond {
lua.push_str(&format!("if {} then\n", c));
lua.push_str(body);
lua.push_str("end\n");
} else {
lua.push_str(body);
}
}
#[derive(Default)]
pub struct LoaderOptions {
pub global_before: Option<String>,
pub global_after: Option<String>,
}
pub fn promote_lazy_to_eager(scripts: &mut [PluginScripts]) -> std::collections::HashSet<String> {
let mut promoted = std::collections::HashSet::new();
let max_iterations = scripts.len() + 1;
for _ in 0..max_iterations {
let eager_names: std::collections::HashSet<String> = scripts
.iter()
.filter(|s| !s.lazy)
.map(|s| s.name.clone())
.collect();
let depended_by_eager: std::collections::HashSet<String> = scripts
.iter()
.filter(|s| !s.lazy)
.flat_map(|s| s.depends.iter().flatten().cloned())
.collect();
let to_promote: Vec<(String, &'static str)> = scripts
.iter()
.filter(|s| s.lazy)
.filter_map(|s| {
if depended_by_eager.contains(&s.name) {
Some((s.name.clone(), "depended on by an eager plugin"))
} else if s
.on_source
.as_ref()
.map(|sources| sources.iter().any(|src| eager_names.contains(src)))
.unwrap_or(false)
{
Some((
s.name.clone(),
"on_source references an eager plugin (event fires before listener is registered)",
))
} else {
None
}
})
.collect();
if to_promote.is_empty() {
break;
}
for (name, reason) in &to_promote {
eprintln!(
"Note: '{}' is lazy but {} — promoting to eager.",
name, reason
);
if let Some(s) = scripts.iter_mut().find(|s| s.name == *name) {
s.lazy = false;
promoted.insert(name.clone());
}
}
}
promoted
}
pub fn generate_loader(
merged_dir: &Path,
scripts: &[PluginScripts],
opts: &LoaderOptions,
) -> String {
let mut scripts = scripts.to_vec();
promote_lazy_to_eager(&mut scripts);
let lazy_names: std::collections::HashSet<String> = scripts
.iter()
.filter(|s| s.lazy)
.map(|s| s.name.clone())
.collect();
let lazy_deps_map: std::collections::HashMap<String, Vec<String>> = scripts
.iter()
.filter(|s| s.lazy)
.filter_map(|s| {
let deps: Vec<String> = s
.depends
.iter()
.flatten()
.filter(|d| lazy_names.contains(*d))
.cloned()
.collect();
if deps.is_empty() {
None
} else {
Some((s.name.clone(), deps))
}
})
.collect();
let mut lua = String::new();
lua.push_str("-- rvpm generated loader.lua\n\n");
lua.push_str("vim.go.loadplugins = false\n\n");
lua.push_str(r#"local function load_lazy(name, path, plugin_files, ftdetect_files, after_plugin_files, before, after)
if _G["rvpm_loaded_" .. name] then return end
_G["rvpm_loaded_" .. name] = true
vim.opt.rtp:append(path)
if before then dofile(before) end
for _, f in ipairs(plugin_files) do vim.cmd("source " .. f) end
if #ftdetect_files > 0 then
vim.cmd("augroup filetypedetect")
for _, f in ipairs(ftdetect_files) do vim.cmd("source " .. f) end
vim.cmd("augroup END")
end
for _, f in ipairs(after_plugin_files) do vim.cmd("source " .. f) end
if after then dofile(after) end
vim.api.nvim_exec_autocmds("User", { pattern = "rvpm_loaded_" .. name })
end
"#);
if let Some(before) = &opts.global_before {
lua.push_str(&format!("dofile(\"{}\")\n\n", before.replace('\\', "/")));
}
for s in &scripts {
if let Some(init) = &s.init {
let body = format!("dofile(\"{}\")\n", init.replace('\\', "/"));
push_with_cond(&mut lua, &s.cond, &body);
}
}
lua.push('\n');
if scripts.iter().any(|s| s.merge) {
let merged_path = merged_dir.to_string_lossy().replace('\\', "/");
lua.push_str(&format!("vim.opt.rtp:append(\"{}\")\n\n", merged_path));
}
for s in &scripts {
if s.lazy {
continue;
}
let mut body = String::new();
let path = s.path.replace('\\', "/");
if !s.merge {
body.push_str(&format!("vim.opt.rtp:append(\"{}\")\n", path));
}
if let Some(before) = &s.before {
body.push_str(&format!("dofile(\"{}\")\n", before.replace('\\', "/")));
}
for f in &s.plugin_files {
body.push_str(&format!("vim.cmd(\"source {}\")\n", f.replace('\\', "/")));
}
if !s.ftdetect_files.is_empty() {
body.push_str("vim.cmd(\"augroup filetypedetect\")\n");
for f in &s.ftdetect_files {
body.push_str(&format!("vim.cmd(\"source {}\")\n", f.replace('\\', "/")));
}
body.push_str("vim.cmd(\"augroup END\")\n");
}
for f in &s.after_plugin_files {
body.push_str(&format!("vim.cmd(\"source {}\")\n", f.replace('\\', "/")));
}
if let Some(after) = &s.after {
body.push_str(&format!("dofile(\"{}\")\n", after.replace('\\', "/")));
}
body.push_str(&format!(
"vim.api.nvim_exec_autocmds(\"User\", {{ pattern = \"rvpm_loaded_{}\" }})\n",
s.name
));
push_with_cond(&mut lua, &s.cond, &body);
}
lua.push('\n');
for s in &scripts {
if !s.lazy {
continue;
}
let path = s.path.replace('\\', "/");
let before = s
.before
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let after = s
.after
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let safe = sanitize_name(&s.name);
let pf_var = format!("_rvpm_pf_{}", safe);
let fd_var = format!("_rvpm_fd_{}", safe);
let ap_var = format!("_rvpm_ap_{}", safe);
let mut body = String::new();
body.push_str("do\n");
body.push_str(&format!(
"local {} = {}\n",
pf_var,
lua_str_list(&s.plugin_files)
));
body.push_str(&format!(
"local {} = {}\n",
fd_var,
lua_str_list(&s.ftdetect_files)
));
body.push_str(&format!(
"local {} = {}\n",
ap_var,
lua_str_list(&s.after_plugin_files)
));
let mut deps_load = String::new();
if let Some(deps) = lazy_deps_map.get(&s.name) {
for dep in deps {
if let Some(dep_script) = scripts.iter().find(|ds| ds.name == *dep) {
let dp = dep_script.path.replace('\\', "/");
let db = dep_script
.before
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let da = dep_script
.after
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let dsafe = sanitize_name(dep);
body.push_str(&format!(
"local _rvpm_pf_{dsafe} = {}\n",
lua_str_list(&dep_script.plugin_files)
));
body.push_str(&format!(
"local _rvpm_fd_{dsafe} = {}\n",
lua_str_list(&dep_script.ftdetect_files)
));
body.push_str(&format!(
"local _rvpm_ap_{dsafe} = {}\n",
lua_str_list(&dep_script.after_plugin_files)
));
deps_load.push_str(&format!(
"load_lazy(\"{dep}\", \"{dp}\", _rvpm_pf_{dsafe}, _rvpm_fd_{dsafe}, _rvpm_ap_{dsafe}, {db}, {da})\n ",
));
}
}
}
let load_call = format!(
"{deps_load}load_lazy(\"{}\", \"{}\", {}, {}, {}, {}, {})",
s.name, path, pf_var, fd_var, ap_var, before, after
);
if let Some(cmds) = &s.on_cmd {
for cmd in cmds {
body.push_str(&format!(
"vim.api.nvim_create_user_command(\"{cmd}\", function(event)\n\
\x20 pcall(vim.api.nvim_del_user_command, \"{cmd}\")\n\
\x20 {load}\n\
\x20 local cmd = {{ cmd = \"{cmd}\", bang = event.bang or nil, mods = event.smods, args = event.fargs }}\n\
\x20 if event.range == 1 then\n\
\x20 cmd.range = {{ event.line1 }}\n\
\x20 elseif event.range == 2 then\n\
\x20 cmd.range = {{ event.line1, event.line2 }}\n\
\x20 end\n\
\x20 if event.count >= 0 and event.range == 0 then\n\
\x20 cmd.count = event.count\n\
\x20 end\n\
\x20 vim.cmd(cmd)\n\
end, {{\n\
\x20 bang = true,\n\
\x20 range = true,\n\
\x20 nargs = \"*\",\n\
\x20 complete = function(_, line)\n\
\x20 pcall(vim.api.nvim_del_user_command, \"{cmd}\")\n\
\x20 {load}\n\
\x20 return vim.fn.getcompletion(line, \"cmdline\")\n\
\x20 end,\n\
}})\n",
cmd = cmd,
load = load_call,
));
}
}
if let Some(fts) = &s.on_ft {
body.push_str(&format!(
"vim.api.nvim_create_autocmd(\"FileType\", {{ pattern = {{ \"{}\" }}, once = true, callback = function(ev)\n\
\x20 {load}\n\
\x20 vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(\"FileType\", {{ buffer = ev.buf, modeline = false }}) end end)\n\
end }})\n",
fts.join("\", \""),
load = load_call,
));
}
if let Some(maps) = &s.on_map {
for m in maps {
let modes = m.modes_or_default();
let modes_lua = lua_str_list(&modes);
let lhs = &m.lhs;
let opts_table = match &m.desc {
Some(d) => format!(", {{ desc = \"{}\" }}", d.replace('"', "\\\"")),
None => String::new(),
};
body.push_str(&format!(
"vim.keymap.set({modes}, \"{lhs}\", function()\n\
\x20 vim.keymap.del({modes}, \"{lhs}\")\n\
\x20 {load}\n\
\x20 local feed = vim.api.nvim_replace_termcodes(\"<Ignore>{lhs}\", true, true, true)\n\
\x20 vim.api.nvim_feedkeys(feed, \"m\", false)\n\
end{opts})\n",
modes = modes_lua,
lhs = lhs,
load = load_call,
opts = opts_table,
));
}
}
if let Some(events) = &s.on_event {
let mut regular: Vec<String> = Vec::new();
let mut user_patterns: Vec<String> = Vec::new();
for e in events {
if let Some(pat) = e.strip_prefix("User ") {
user_patterns.push(pat.trim().to_string());
} else {
regular.push(e.clone());
}
}
if !regular.is_empty() {
body.push_str(&format!(
"vim.api.nvim_create_autocmd({{ \"{}\" }}, {{ once = true, callback = function(ev)\n\
\x20 {load}\n\
\x20 vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(ev.event, {{ buffer = ev.buf, data = ev.data, modeline = false }}) end end)\n\
end }})\n",
regular.join("\", \""),
load = load_call,
));
}
for pat in &user_patterns {
body.push_str(&format!(
"vim.api.nvim_create_autocmd(\"User\", {{ pattern = \"{pat}\", once = true, callback = function(ev)\n\
\x20 {load}\n\
\x20 vim.schedule(function() vim.api.nvim_exec_autocmds(\"User\", {{ pattern = \"{pat}\", data = ev.data, modeline = false }}) end)\n\
end }})\n",
pat = pat,
load = load_call,
));
}
}
if let Some(paths) = &s.on_path {
body.push_str(&format!(
"vim.api.nvim_create_autocmd({{ \"BufRead\", \"BufNewFile\" }}, {{ pattern = {{ \"{}\" }}, once = true, callback = function(ev)\n\
\x20 {load}\n\
\x20 vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(ev.event, {{ buffer = ev.buf, data = ev.data, modeline = false }}) end end)\n\
end }})\n",
paths.join("\", \""),
load = load_call,
));
}
if let Some(sources) = &s.on_source {
let patterns: Vec<String> = sources
.iter()
.map(|src| format!("rvpm_loaded_{}", src))
.collect();
body.push_str(&format!(
"vim.api.nvim_create_autocmd(\"User\", {{ pattern = {{ \"{}\" }}, once = true, callback = function()\n\
\x20 {load}\n\
end }})\n",
patterns.join("\", \""),
load = load_call,
));
}
body.push_str("end\n");
push_with_cond(&mut lua, &s.cond, &body);
}
{
let mut cs_entries: Vec<String> = Vec::new();
for s in &scripts {
if !s.lazy || s.colorschemes.is_empty() {
continue;
}
let path = s.path.replace('\\', "/");
let before = s
.before
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let after = s
.after
.as_ref()
.map(|p| format!("\"{}\"", p.replace('\\', "/")))
.unwrap_or_else(|| "nil".to_string());
let pf_inline = lua_str_list(&s.plugin_files);
let fd_inline = lua_str_list(&s.ftdetect_files);
let ap_inline = lua_str_list(&s.after_plugin_files);
for cs in &s.colorschemes {
cs_entries.push(format!(
"[\"{cs}\"] = function() load_lazy(\"{name}\", \"{path}\", {pf}, {fd}, {ap}, {before}, {after}) end",
cs = cs,
name = s.name,
path = path,
pf = pf_inline,
fd = fd_inline,
ap = ap_inline,
before = before,
after = after,
));
}
}
if !cs_entries.is_empty() {
lua.push_str("local _rvpm_colorschemes = {\n");
for entry in &cs_entries {
lua.push_str(&format!(" {},\n", entry));
}
lua.push_str("}\n");
lua.push_str(
"vim.api.nvim_create_autocmd(\"ColorSchemePre\", {\n\
\x20 callback = function(ev)\n\
\x20 local loader = _rvpm_colorschemes[ev.match]\n\
\x20 if loader then loader() end\n\
\x20 end,\n\
})\n\n",
);
}
}
if let Some(after) = &opts.global_after {
lua.push_str(&format!("\ndofile(\"{}\")\n", after.replace('\\', "/")));
}
lua
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_eager_depending_on_lazy_promotes_to_eager() {
let mut a = PluginScripts::for_test("snacks.nvim", "/path/snacks");
a.lazy = true;
a.on_cmd = Some(vec!["Snacks".to_string()]);
a.plugin_files = vec!["/path/snacks/plugin/snacks.lua".to_string()];
let mut b = PluginScripts::for_test("telescope.nvim", "/path/telescope");
b.lazy = false;
b.depends = Some(vec!["snacks.nvim".to_string()]);
b.plugin_files = vec!["/path/telescope/plugin/telescope.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b]);
let snacks_source = lua
.find("source /path/snacks/plugin/snacks.lua")
.expect("snacks should be sourced eagerly");
let telescope_source = lua
.find("source /path/telescope/plugin/telescope.lua")
.expect("telescope should be sourced");
assert!(
snacks_source < telescope_source,
"snacks (promoted eager) must load before telescope"
);
assert!(
!lua.contains("nvim_create_user_command(\"Snacks\""),
"promoted plugin should not register lazy triggers"
);
}
#[test]
fn test_on_source_referencing_eager_promotes_to_eager() {
let mut b = PluginScripts::for_test("snacks.nvim", "/path/snacks");
b.lazy = false;
b.plugin_files = vec!["/path/snacks/plugin/snacks.lua".to_string()];
let mut a = PluginScripts::for_test("telescope.nvim", "/path/telescope");
a.lazy = true;
a.on_source = Some(vec!["snacks.nvim".to_string()]);
a.plugin_files = vec!["/path/telescope/plugin/telescope.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[b, a]);
assert!(
lua.contains("source /path/telescope/plugin/telescope.lua"),
"on_source→eager plugin should be promoted and sourced eagerly"
);
assert!(
!lua.contains("rvpm_loaded_snacks.nvim\", once = true"),
"promoted plugin should not register on_source trigger"
);
}
#[test]
fn test_on_source_chain_promotion() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = false;
a.plugin_files = vec!["/path/a/plugin/a.lua".to_string()];
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.on_source = Some(vec!["a".to_string()]);
b.plugin_files = vec!["/path/b/plugin/b.lua".to_string()];
let mut c = PluginScripts::for_test("c", "/path/c");
c.lazy = true;
c.on_source = Some(vec!["b".to_string()]);
c.plugin_files = vec!["/path/c/plugin/c.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b, c]);
assert!(lua.contains("source /path/a/plugin/a.lua"));
assert!(lua.contains("source /path/b/plugin/b.lua"));
assert!(lua.contains("source /path/c/plugin/c.lua"));
let pos_a = lua.find("source /path/a/plugin/a.lua").unwrap();
let pos_b = lua.find("source /path/b/plugin/b.lua").unwrap();
let pos_c = lua.find("source /path/c/plugin/c.lua").unwrap();
assert!(pos_a < pos_b, "a must load before b");
assert!(pos_b < pos_c, "b must load before c");
}
#[test]
fn test_lazy_depending_on_lazy_loads_deps_first() {
let mut a = PluginScripts::for_test("snacks.nvim", "/path/snacks");
a.lazy = true;
a.plugin_files = vec!["/path/snacks/plugin/snacks.lua".to_string()];
let mut b = PluginScripts::for_test("telescope.nvim", "/path/telescope");
b.lazy = true;
b.on_cmd = Some(vec!["Telescope".to_string()]);
b.depends = Some(vec!["snacks.nvim".to_string()]);
b.plugin_files = vec!["/path/telescope/plugin/telescope.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b]);
let trigger_section = lua
.find("nvim_create_user_command(\"Telescope\"")
.expect("telescope trigger missing");
let after_trigger = &lua[trigger_section..];
assert!(
after_trigger.contains("load_lazy(\"snacks.nvim\""),
"telescope trigger should load snacks.nvim dependency first:\n{}",
&after_trigger[..500.min(after_trigger.len())]
);
}
#[test]
fn test_mixed_depends_and_on_source() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = false;
a.depends = Some(vec!["b".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.lua".to_string()];
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.plugin_files = vec!["/path/b/plugin/b.lua".to_string()];
let mut c = PluginScripts::for_test("c", "/path/c");
c.lazy = true;
c.on_source = Some(vec!["b".to_string()]);
c.plugin_files = vec!["/path/c/plugin/c.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[b, a, c]);
assert!(lua.contains("source /path/a/plugin/a.lua"));
assert!(lua.contains("source /path/b/plugin/b.lua"));
assert!(lua.contains("source /path/c/plugin/c.lua"));
let pb = lua.find("source /path/b/plugin/b.lua").unwrap();
let pa = lua.find("source /path/a/plugin/a.lua").unwrap();
let pc = lua.find("source /path/c/plugin/c.lua").unwrap();
assert!(pb < pa, "b must load before a (a depends on b)");
assert!(pb < pc, "b must load before c (c on_source b)");
}
#[test]
fn test_circular_on_source_does_not_infinite_loop() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.on_source = Some(vec!["b".to_string()]);
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.on_source = Some(vec!["a".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[a, b]);
assert!(!lua.contains("source /path/a/plugin"), "a should stay lazy");
assert!(!lua.contains("source /path/b/plugin"), "b should stay lazy");
}
#[test]
fn test_circular_depends_does_not_infinite_loop() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.depends = Some(vec!["b".to_string()]);
a.on_cmd = Some(vec!["FooA".to_string()]);
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.depends = Some(vec!["a".to_string()]);
b.on_cmd = Some(vec!["FooB".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[a, b]);
assert!(
lua.contains("nvim_create_user_command(\"FooA\""),
"a trigger exists"
);
assert!(
lua.contains("nvim_create_user_command(\"FooB\""),
"b trigger exists"
);
}
#[test]
fn test_circular_depends_with_eager_involved() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = false;
a.depends = Some(vec!["b".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.lua".to_string()];
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.depends = Some(vec!["a".to_string()]);
b.plugin_files = vec!["/path/b/plugin/b.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b]);
assert!(
lua.contains("source /path/b/plugin/b.lua"),
"b should be promoted"
);
assert!(
lua.contains("source /path/a/plugin/a.lua"),
"a should be sourced"
);
}
#[test]
fn test_three_way_circular_depends() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.depends = Some(vec!["c".to_string()]);
a.on_cmd = Some(vec!["FooA".to_string()]);
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.depends = Some(vec!["a".to_string()]);
b.on_cmd = Some(vec!["FooB".to_string()]);
let mut c = PluginScripts::for_test("c", "/path/c");
c.lazy = true;
c.depends = Some(vec!["b".to_string()]);
c.on_cmd = Some(vec!["FooC".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[a, b, c]);
assert!(lua.contains("nvim_create_user_command(\"FooA\""));
assert!(lua.contains("nvim_create_user_command(\"FooB\""));
assert!(lua.contains("nvim_create_user_command(\"FooC\""));
}
#[test]
fn test_self_referential_depends_does_not_crash() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.depends = Some(vec!["a".to_string()]);
let _lua = gen_loader(Path::new("/merged"), &[a]);
}
#[test]
fn test_eager_with_on_source_is_harmless() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = false;
a.on_source = Some(vec!["nonexistent".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
assert!(lua.contains("source /path/a/plugin/a.lua"));
assert!(!lua.contains("rvpm_loaded_nonexistent"));
}
#[test]
fn test_reverse_depends_on_source_combo() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.on_cmd = Some(vec!["FooA".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.lua".to_string()];
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.depends = Some(vec!["a".to_string()]);
b.on_source = Some(vec!["a".to_string()]);
b.plugin_files = vec!["/path/b/plugin/b.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b]);
assert!(
lua.contains("nvim_create_user_command(\"FooA\""),
"A trigger exists"
);
assert!(
lua.contains("rvpm_loaded_a"),
"B on_source trigger for A exists"
);
}
#[test]
fn test_lazy_plugin_with_colorschemes_emits_colorscheme_pre_handler() {
let mut s = PluginScripts::for_test("catppuccin", "/path/catppuccin");
s.lazy = true;
s.colorschemes = vec!["catppuccin".to_string(), "catppuccin-latte".to_string()];
s.plugin_files = vec!["/path/catppuccin/plugin/catppuccin.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("ColorSchemePre"),
"should register ColorSchemePre autocmd for lazy colorscheme"
);
assert!(lua.contains("catppuccin"), "should reference catppuccin");
assert!(
lua.contains("catppuccin-latte"),
"should reference catppuccin-latte"
);
}
#[test]
fn test_eager_plugin_with_colorschemes_no_handler() {
let mut s = PluginScripts::for_test("catppuccin", "/path/catppuccin");
s.lazy = false;
s.colorschemes = vec!["catppuccin".to_string()];
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
!lua.contains("ColorSchemePre"),
"eager plugin should NOT register ColorSchemePre handler"
);
}
#[test]
fn test_multiple_lazy_colorscheme_plugins_combined_handler() {
let mut a = PluginScripts::for_test("catppuccin", "/path/catppuccin");
a.lazy = true;
a.colorschemes = vec!["catppuccin".to_string()];
a.plugin_files = vec!["/path/catppuccin/plugin/c.lua".to_string()];
let mut b = PluginScripts::for_test("tokyonight", "/path/tokyonight");
b.lazy = true;
b.colorschemes = vec!["tokyonight".to_string(), "tokyonight-night".to_string()];
b.plugin_files = vec!["/path/tokyonight/plugin/t.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a, b]);
assert!(lua.contains("catppuccin"));
assert!(lua.contains("tokyonight"));
assert!(lua.contains("tokyonight-night"));
}
#[test]
fn test_colorscheme_handler_loads_correct_plugin() {
let mut s = PluginScripts::for_test("catppuccin", "/path/catppuccin");
s.lazy = true;
s.colorschemes = vec!["catppuccin".to_string()];
s.plugin_files = vec!["/path/catppuccin/plugin/c.lua".to_string()];
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("load_lazy(\"catppuccin\""),
"ColorSchemePre handler should call load_lazy for the matching plugin"
);
}
#[test]
fn test_loader_global_before_runs_before_all_plugins() {
let mut s = PluginScripts::for_test("a", "/path/a");
s.init = Some("/cfg/a/init.lua".to_string());
let opts = LoaderOptions {
global_before: Some("/rvpm/before.lua".to_string()),
global_after: None,
};
let lua = generate_loader(Path::new("/merged"), &[s], &opts);
let before_pos = lua.find("/rvpm/before.lua").expect("global before missing");
let init_pos = lua.find("/cfg/a/init.lua").expect("plugin init missing");
assert!(
before_pos < init_pos,
"global before must run BEFORE any plugin init"
);
}
#[test]
fn test_loader_global_after_runs_after_all_lazy_triggers() {
let mut s = PluginScripts::for_test("a", "/path/a");
s.lazy = true;
s.on_cmd = Some(vec!["Foo".to_string()]);
let opts = LoaderOptions {
global_before: None,
global_after: Some("/rvpm/after.lua".to_string()),
};
let lua = generate_loader(Path::new("/merged"), &[s], &opts);
let trigger_pos = lua
.find("nvim_create_user_command")
.expect("trigger missing");
let after_pos = lua.find("/rvpm/after.lua").expect("global after missing");
assert!(
trigger_pos < after_pos,
"global after must run AFTER lazy trigger registrations"
);
}
#[test]
fn test_loader_no_global_hooks_when_none() {
let opts = LoaderOptions {
global_before: None,
global_after: None,
};
let lua = generate_loader(Path::new("/merged"), &[], &opts);
assert!(
!lua.contains("dofile") || lua.contains("load_lazy"),
"no dofile for global hooks when None"
);
}
#[test]
fn test_loader_disables_neovim_plugin_loading() {
let lua = gen_loader(Path::new("/merged"), &[]);
assert!(
lua.contains("vim.go.loadplugins = false"),
"loader must disable Neovim's default plugin loading"
);
}
#[test]
fn test_loader_phase_order_init_rtp_before() {
let mut s = PluginScripts::for_test("a", "/path/a");
s.merge = true;
s.init = Some("/cfg/a/init.lua".to_string());
s.before = Some("/cfg/a/before.lua".to_string());
let lua = gen_loader(Path::new("/merged"), &[s]);
let init_pos = lua.find("/cfg/a/init.lua").expect("init missing");
let rtp_pos = lua
.find("vim.opt.rtp:append(\"/merged\")")
.expect("merged rtp missing");
let before_pos = lua.find("/cfg/a/before.lua").expect("before missing");
assert!(
init_pos < rtp_pos,
"init must come BEFORE merged rtp append"
);
assert!(rtp_pos < before_pos, "before must come AFTER rtp append");
}
#[test]
fn test_loader_merged_rtp_appended_exactly_once() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.merge = true;
let mut b = PluginScripts::for_test("b", "/path/b");
b.merge = true;
let lua = gen_loader(Path::new("/merged"), &[a, b]);
let count = lua.matches("vim.opt.rtp:append(\"/merged\")").count();
assert_eq!(
count, 1,
"merged rtp should be appended exactly once for multiple merge=true plugins"
);
}
#[test]
fn test_loader_no_merged_rtp_when_all_non_merge() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.merge = false;
let lua = gen_loader(Path::new("/merged"), &[a]);
assert!(
!lua.contains("vim.opt.rtp:append(\"/merged\")"),
"should NOT append merged rtp when no merge=true plugin exists"
);
}
#[test]
fn test_loader_non_merge_eager_appends_own_rtp() {
let mut a = PluginScripts::for_test("solo", "/path/solo");
a.merge = false;
let lua = gen_loader(Path::new("/merged"), &[a]);
assert!(
lua.contains("vim.opt.rtp:append(\"/path/solo\")"),
"non-merge eager plugin must append its own path to rtp"
);
}
#[test]
fn test_loader_eager_sources_plugin_files_between_before_and_after() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.merge = true;
a.before = Some("/cfg/a/before.lua".to_string());
a.after = Some("/cfg/a/after.lua".to_string());
a.plugin_files = vec!["/path/a/plugin/a.vim".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
let before_pos = lua.find("/cfg/a/before.lua").unwrap();
let source_pos = lua
.find("vim.cmd(\"source /path/a/plugin/a.vim\")")
.expect("plugin file source missing");
let after_pos = lua.find("/cfg/a/after.lua").unwrap();
assert!(
before_pos < source_pos,
"before.lua must come before plugin/ source"
);
assert!(
source_pos < after_pos,
"after.lua must come after plugin/ source"
);
}
#[test]
fn test_loader_eager_wraps_ftdetect_in_filetypedetect_augroup() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.merge = true;
a.ftdetect_files = vec!["/path/a/ftdetect/a.vim".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
let ftdetect_source_pos = lua
.find("vim.cmd(\"source /path/a/ftdetect/a.vim\")")
.expect("ftdetect source missing");
let prior = &lua[..ftdetect_source_pos];
let augroup_begin_pos = prior
.rfind("augroup filetypedetect")
.expect("augroup begin missing before ftdetect source");
let after = &lua[ftdetect_source_pos..];
let augroup_end_rel = after
.find("augroup END")
.expect("augroup END missing after ftdetect source");
assert!(augroup_begin_pos < ftdetect_source_pos);
assert!(augroup_end_rel > 0);
}
#[test]
fn test_loader_eager_sources_after_plugin_files() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.merge = true;
a.after_plugin_files = vec!["/path/a/after/plugin/a.vim".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
assert!(
lua.contains("vim.cmd(\"source /path/a/after/plugin/a.vim\")"),
"after/plugin files must be sourced"
);
}
#[test]
fn test_loader_no_plugin_files_emitted_for_lazy_plugin() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.merge = false;
a.on_cmd = Some(vec!["Foo".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.vim".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
let direct_source_count = lua
.lines()
.filter(|l| {
l.trim_start()
.starts_with("vim.cmd(\"source /path/a/plugin/a.vim\")")
})
.count();
assert_eq!(
direct_source_count, 0,
"lazy plugin files must not be sourced eagerly at top level"
);
}
#[test]
fn test_loader_lazy_trigger_passes_file_lists_to_load_lazy() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
a.on_cmd = Some(vec!["Foo".to_string()]);
a.plugin_files = vec!["/path/a/plugin/a.vim".to_string()];
a.ftdetect_files = vec!["/path/a/ftdetect/a.vim".to_string()];
a.after_plugin_files = vec!["/path/a/after/plugin/a.vim".to_string()];
let lua = gen_loader(Path::new("/merged"), &[a]);
assert!(
lua.contains("/path/a/plugin/a.vim"),
"plugin file must be referenced"
);
assert!(
lua.contains("/path/a/ftdetect/a.vim"),
"ftdetect file must be referenced"
);
assert!(
lua.contains("/path/a/after/plugin/a.vim"),
"after/plugin file must be referenced"
);
}
#[test]
fn test_load_lazy_helper_sources_ftdetect_in_augroup() {
let lua = gen_loader(Path::new("/merged"), &[]);
let load_lazy_start = lua
.find("local function load_lazy")
.expect("load_lazy definition missing");
let end_marker = lua[load_lazy_start..]
.find("\nend\n")
.expect("load_lazy end missing")
+ load_lazy_start;
let body = &lua[load_lazy_start..end_marker];
assert!(
body.contains("augroup filetypedetect"),
"load_lazy must wrap ftdetect in filetypedetect augroup"
);
assert!(
body.contains("augroup END"),
"load_lazy must close the augroup"
);
}
fn gen_loader(merged: &Path, scripts: &[PluginScripts]) -> String {
generate_loader(merged, scripts, &LoaderOptions::default())
}
fn make_lazy_plugin(name: &str) -> PluginScripts {
let mut s = PluginScripts::for_test(name, &format!("/path/{}", name));
s.lazy = true;
s
}
#[test]
fn test_on_cmd_handler_has_bang_range_complete_options() {
let mut s = make_lazy_plugin("tel");
s.on_cmd = Some(vec!["Telescope".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(lua.contains("bang = true"), "on_cmd must enable bang");
assert!(lua.contains("range = true"), "on_cmd must enable range");
assert!(
lua.contains("complete ="),
"on_cmd must provide complete callback"
);
assert!(
lua.contains("nargs = \"*\""),
"on_cmd still supports any args"
);
}
#[test]
fn test_on_cmd_handler_reconstructs_command_from_event() {
let mut s = make_lazy_plugin("tel");
s.on_cmd = Some(vec!["Telescope".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(lua.contains("event.bang"), "should read event.bang");
assert!(lua.contains("event.smods"), "should read event.smods");
assert!(lua.contains("event.fargs"), "should read event.fargs");
assert!(lua.contains("event.range"), "should read event.range");
assert!(lua.contains("event.count"), "should read event.count");
assert!(
lua.contains("vim.cmd(cmd)") || lua.contains("vim.cmd(_rvpm_cmd"),
"should dispatch via vim.cmd(table), not string concatenation"
);
}
#[test]
fn test_on_cmd_handler_complete_loads_plugin_and_delegates() {
let mut s = make_lazy_plugin("tel");
s.on_cmd = Some(vec!["Telescope".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.fn.getcompletion"),
"complete callback should delegate to vim.fn.getcompletion"
);
}
#[test]
fn test_on_ft_handler_retriggers_filetype_event_after_load() {
let mut s = make_lazy_plugin("nvim-rust");
s.on_ft = Some(vec!["rust".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(\"FileType\""),
"on_ft callback must re-trigger FileType via vim.schedule with buf validity check"
);
assert!(
lua.contains("buffer = ev.buf"),
"re-trigger must use the original buffer"
);
}
#[test]
fn test_on_event_handler_refires_event_with_buffer_and_data() {
let mut s = make_lazy_plugin("lsp");
s.on_event = Some(vec!["BufReadPre".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(ev.event"),
"on_event callback must re-fire via vim.schedule with buf validity check"
);
assert!(lua.contains("buffer = ev.buf"));
assert!(lua.contains("data = ev.data"));
}
#[test]
fn test_on_event_user_prefix_creates_user_autocmd_with_pattern() {
let mut s = make_lazy_plugin("lazyvim-extras");
s.on_event = Some(vec!["User LazyVimStarted".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("nvim_create_autocmd(\"User\""),
"User event must create a User autocmd"
);
assert!(
lua.contains("pattern = \"LazyVimStarted\"")
|| lua.contains("pattern = { \"LazyVimStarted\" }"),
"User event must specify the pattern"
);
}
#[test]
fn test_on_event_mixes_regular_and_user_events() {
let mut s = make_lazy_plugin("mixed");
s.on_event = Some(vec![
"BufReadPre".to_string(),
"User LazyVimStarted".to_string(),
]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("BufReadPre"),
"regular event BufReadPre must still be registered"
);
assert!(
lua.contains("nvim_create_autocmd(\"User\""),
"User event must also be registered"
);
assert!(
lua.contains("LazyVimStarted"),
"User pattern must be present"
);
}
#[test]
fn test_on_path_handler_refires_event_after_load() {
let mut s = make_lazy_plugin("rust-tools");
s.on_path = Some(vec!["*.rs".to_string()]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.schedule(function() if vim.api.nvim_buf_is_valid(ev.buf) then vim.api.nvim_exec_autocmds(ev.event"),
"on_path callback must re-fire via vim.schedule with buf validity check"
);
assert!(lua.contains("buffer = ev.buf"));
}
#[test]
fn test_on_map_handler_uses_ignore_prefix_feedkeys() {
let mut s = make_lazy_plugin("keyed");
s.on_map = Some(vec![MapSpec {
lhs: "<leader>f".to_string(),
mode: Vec::new(),
desc: None,
}]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("<Ignore>"),
"on_map replay must use <Ignore> prefix (lazy.nvim pattern)"
);
}
#[test]
fn test_on_map_simple_form_defaults_to_normal_mode() {
let mut s = make_lazy_plugin("p");
s.on_map = Some(vec![MapSpec {
lhs: "<leader>f".to_string(),
mode: Vec::new(),
desc: None,
}]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.keymap.set({ \"n\" }"),
"empty mode should default to normal mode"
);
}
#[test]
fn test_on_map_table_form_respects_multiple_modes() {
let mut s = make_lazy_plugin("p");
s.on_map = Some(vec![MapSpec {
lhs: "<leader>v".to_string(),
mode: vec!["n".to_string(), "x".to_string()],
desc: None,
}]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("vim.keymap.set({ \"n\", \"x\" }"),
"multiple modes should be emitted as a Lua list"
);
}
#[test]
fn test_on_map_table_form_emits_desc_opts() {
let mut s = make_lazy_plugin("p");
s.on_map = Some(vec![MapSpec {
lhs: "<leader>g".to_string(),
mode: vec!["n".to_string()],
desc: Some("Grep files".to_string()),
}]);
let lua = gen_loader(Path::new("/merged"), &[s]);
assert!(
lua.contains("desc = \"Grep files\""),
"desc should be emitted in keymap opts"
);
}
#[test]
#[ignore]
fn dump_full_sample_loader() {
let mut plenary = PluginScripts::for_test(
"plenary",
"/cache/rvpm/repos/github.com/nvim-lua/plenary.nvim",
);
plenary.merge = true;
plenary.init = Some("/config/init.lua".to_string());
plenary.plugin_files = vec![
"/cache/rvpm/repos/github.com/nvim-lua/plenary.nvim/plugin/plenary.vim".to_string(),
];
let mut telescope = PluginScripts::for_test(
"telescope",
"/cache/rvpm/repos/github.com/nvim-telescope/telescope.nvim",
);
telescope.merge = true;
telescope.lazy = true;
telescope.before = Some("/config/tel/before.lua".to_string());
telescope.after = Some("/config/tel/after.lua".to_string());
telescope.on_cmd = Some(vec!["Telescope".to_string()]);
telescope.on_source = Some(vec!["plenary".to_string()]);
telescope.on_event = Some(vec![
"BufReadPre".to_string(),
"User LazyVimStarted".to_string(),
]);
telescope.on_map = Some(vec![
MapSpec {
lhs: "<leader>ff".to_string(),
mode: vec!["n".to_string()],
desc: Some("Find files".to_string()),
},
MapSpec {
lhs: "<leader>fg".to_string(),
mode: vec!["n".to_string(), "x".to_string()],
desc: None,
},
MapSpec {
lhs: "<leader>fb".to_string(),
mode: Vec::new(),
desc: None,
},
]);
telescope.plugin_files = vec![
"/cache/rvpm/repos/github.com/nvim-telescope/telescope.nvim/plugin/telescope.lua"
.to_string(),
];
telescope.ftdetect_files = vec![];
telescope.after_plugin_files = vec![];
let mut treesitter = PluginScripts::for_test(
"nvim-treesitter",
"/cache/rvpm/repos/github.com/nvim-treesitter/nvim-treesitter",
);
treesitter.merge = false; treesitter.before = Some("/config/ts/before.lua".to_string());
treesitter.after = Some("/config/ts/after.lua".to_string());
treesitter.plugin_files = vec![
"/cache/rvpm/repos/github.com/nvim-treesitter/nvim-treesitter/plugin/nvim-treesitter.lua".to_string(),
];
treesitter.ftdetect_files = vec![
"/cache/rvpm/repos/github.com/nvim-treesitter/nvim-treesitter/ftdetect/blade.vim"
.to_string(),
];
treesitter.after_plugin_files = vec![
"/cache/rvpm/repos/github.com/nvim-treesitter/nvim-treesitter/after/plugin/query_predicates.lua".to_string(),
];
let scripts = vec![plenary, telescope, treesitter];
let lua = gen_loader(Path::new("/cache/rvpm/merged"), &scripts);
println!(
"\n======== GENERATED LOADER ========\n{}\n==================================\n",
lua
);
}
#[test]
fn test_load_lazy_fires_user_event() {
let merged_dir = Path::new("/merged");
let mut s = PluginScripts::for_test("plenary", "/path/plenary");
s.lazy = true;
s.on_cmd = Some(vec!["Plenary".to_string()]);
let lua = gen_loader(merged_dir, &[s]);
assert!(
lua.contains(
"vim.api.nvim_exec_autocmds(\"User\", { pattern = \"rvpm_loaded_\" .. name })"
),
"load_lazy must fire User autocmd after loading"
);
}
#[test]
fn test_generate_loader_with_cond() {
let merged_dir = Path::new("/path/to/merged");
let mut s = PluginScripts::for_test("cond_lazy", "/path/to/plugin");
s.lazy = true;
s.on_cmd = Some(vec!["Cmd".to_string()]);
s.on_ft = Some(vec!["rust".to_string()]);
s.on_map = Some(vec![MapSpec {
lhs: "<leader>f".to_string(),
mode: Vec::new(),
desc: None,
}]);
s.on_event = Some(vec!["BufRead".to_string()]);
s.on_path = Some(vec!["*.rs".to_string(), "Cargo.toml".to_string()]);
s.on_source = Some(vec!["plenary.nvim".to_string()]);
s.cond = Some("vim.fn.has('win32') == 1".to_string());
let lua = gen_loader(merged_dir, &[s]);
assert!(lua.contains("if vim.fn.has('win32') == 1 then"));
assert!(lua.contains("nvim_create_user_command(\"Cmd\""));
assert!(lua.contains("pattern = { \"rust\" }"));
assert!(lua.contains("vim.keymap.set({ \"n\" }, \"<leader>f\""));
assert!(lua.contains("nvim_create_autocmd({ \"BufRead\" }"));
assert!(lua.contains("pattern = { \"*.rs\", \"Cargo.toml\" }"));
assert!(lua.contains("pattern = { \"rvpm_loaded_plenary.nvim\" }"));
}
#[test]
fn test_promote_lazy_to_eager_returns_promoted_names() {
let mut a = PluginScripts::for_test("plenary.nvim", "/path/plenary");
a.lazy = true;
a.merge = true;
let mut b = PluginScripts::for_test("telescope.nvim", "/path/telescope");
b.lazy = false;
b.depends = Some(vec!["plenary.nvim".to_string()]);
let mut scripts = vec![a, b];
let promoted = promote_lazy_to_eager(&mut scripts);
assert!(promoted.contains("plenary.nvim"));
assert_eq!(promoted.len(), 1);
assert!(!scripts[0].lazy, "plenary should be promoted to eager");
assert!(!scripts[1].lazy, "telescope should remain eager");
}
#[test]
fn test_promote_lazy_to_eager_chain() {
let mut a = PluginScripts::for_test("a", "/path/a");
a.lazy = true;
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.depends = Some(vec!["a".to_string()]);
let mut c = PluginScripts::for_test("c", "/path/c");
c.lazy = false;
c.depends = Some(vec!["b".to_string()]);
let mut scripts = vec![a, b, c];
let promoted = promote_lazy_to_eager(&mut scripts);
assert!(promoted.contains("a"));
assert!(promoted.contains("b"));
assert!(!scripts[0].lazy);
assert!(!scripts[1].lazy);
}
#[test]
fn test_promote_lazy_to_eager_no_promotion_needed() {
let a = PluginScripts::for_test("a", "/path/a");
let mut b = PluginScripts::for_test("b", "/path/b");
b.lazy = true;
b.on_cmd = Some(vec!["Cmd".to_string()]);
let mut scripts = vec![a, b];
let promoted = promote_lazy_to_eager(&mut scripts);
assert!(promoted.is_empty());
assert!(!scripts[0].lazy);
assert!(scripts[1].lazy);
}
}