use crate::ported::lib::dict::mergedicts;
use serde_json::{Map, Value};
use std::sync::Mutex;
use std::sync::OnceLock;
pub fn pycmd() -> &'static Mutex<Option<String>> {
static M: OnceLock<Mutex<Option<String>>> = OnceLock::new();
M.get_or_init(|| Mutex::new(None))
}
pub fn set_pycmd(new_pycmd: impl Into<String>) {
let mut slot = pycmd().lock().unwrap_or_else(|e| e.into_inner());
*slot = Some(new_pycmd.into());
}
pub fn get_default_pycmd() -> &'static str {
"python3"
}
pub fn _override_from(
config: &mut Map<String, Value>,
key: Option<&str>,
vim_get_var: impl FnOnce() -> Option<Value>,
) {
let overrides = match vim_get_var() {
Some(v) => v,
None => return,
};
let overlay = if let Some(k) = key {
match overrides.get(k) {
Some(v) => v.clone(),
None => return,
}
} else {
overrides
};
if let Some(overlay_map) = overlay.as_object() {
mergedicts(config, overlay_map.clone(), false);
}
}
pub struct VimVarHandler {
pub vim_varname: String,
pub captured: Vec<String>,
}
impl VimVarHandler {
pub fn new(varname: impl Into<String>) -> Self {
Self {
vim_varname: varname.into(),
captured: Vec::new(),
}
}
pub fn emit(&mut self, message: &str, exc_text: Option<&str>) {
let mut combined = message.to_string();
if let Some(exc) = exc_text {
combined.push('\n');
combined.push_str(exc);
}
self.captured.push(combined);
}
}
pub fn create_window_statusline_format(pyeval: &str) -> impl Fn(u64) -> String + '_ {
move |idx: u64| -> String { format!("%!{}('powerline.statusline({})')", pyeval, idx) }
}
pub fn get_matcher_module(match_name: &str, ext: &str) -> (String, String) {
if let Some(dot_idx) = match_name.rfind('.') {
let (module, rest) = match_name.split_at(dot_idx);
let function = &rest[1..]; (module.to_string(), function.to_string())
} else {
(
format!("powerline.matchers.{}", ext),
match_name.to_string(),
)
}
}
pub struct VimPowerline {
pub last_window_id: u64,
pub pyeval: String,
pub local_themes: Vec<(String, Map<String, Value>)>,
}
impl Default for VimPowerline {
fn default() -> Self {
Self::new("PowerlinePyeval")
}
}
impl VimPowerline {
pub fn new(pyeval: impl Into<String>) -> Self {
Self {
last_window_id: 1,
pyeval: pyeval.into(),
local_themes: Vec::new(),
}
}
pub fn add_local_theme(&mut self, key: impl Into<String>, config: Map<String, Value>) -> bool {
self.local_themes.push((key.into(), config));
true
}
pub fn failed_unicode_message(window_id: u64) -> String {
format!("No window {}", window_id)
}
pub fn setup_components(&self, components: Option<&[&str]>) -> Vec<String> {
let defaults = ["statusline", "tabline"];
let comps = components.unwrap_or(&defaults);
let mut out: Vec<String> = Vec::new();
for c in comps {
if *c == "statusline" {
out.push(format!(
"set statusline=%!{}('powerline.new_window()')",
self.pyeval
));
}
if *c == "tabline" {
out.push(format!(
"set tabline=%!{}('powerline.tabline()')",
self.pyeval
));
}
}
out
}
pub fn reset_highlight<F>(reset_renderer: Option<F>)
where
F: FnOnce(),
{
if let Some(f) = reset_renderer {
f();
}
}
pub fn assign_window_id(
existing: Option<u64>,
last_window_id: u64,
conflict: bool,
) -> (u64, u64, bool) {
match existing {
Some(id) if !conflict => (id, last_window_id, false),
_ => (last_window_id, last_window_id + 1, true),
}
}
pub fn tabline_fallback_window(
current_window_existing_id: Option<u64>,
current_window_number: u64,
last_window_id: u64,
) -> (u64, u64) {
let window_id = current_window_existing_id.unwrap_or(last_window_id);
(window_id, current_window_number)
}
pub fn do_pyeval_command(json_encoded_result: &str) -> String {
format!("return {}", json_encoded_result)
}
pub fn statusline_no_window_message(window_id: Option<u64>) -> Option<String> {
Some(format!("No window {}", window_id.unwrap_or(0)))
}
pub fn get_local_themes<R, L>(
local_themes: &Map<String, Value>,
mut resolve_matcher: R,
mut load_theme: L,
) -> Map<String, Value>
where
R: FnMut(&str) -> Option<String>,
L: FnMut(&str) -> Value,
{
if local_themes.is_empty() {
return Map::new();
}
let mut out = Map::new();
for (k, v) in local_themes {
let matcher_key = if k == "__tabline__" {
Some("__tabline__".to_string())
} else {
resolve_matcher(k)
};
if let Some(matcher) = matcher_key {
let val_str = v.as_str().unwrap_or("");
let theme = load_theme(val_str);
let mut entry = Map::new();
entry.insert("config".to_string(), theme);
out.insert(matcher, Value::Object(entry));
}
}
out
}
pub fn get_config_paths(
powerline_config_paths: Option<Vec<String>>,
super_config_paths: impl FnOnce() -> Vec<String>,
) -> Vec<String> {
match powerline_config_paths {
Some(p) => p,
None => super_config_paths(),
}
}
pub fn new_window<W>(win_idx_none: W) -> Option<(u64, u64)>
where
W: FnOnce() -> Option<(u64, u64)>,
{
win_idx_none()
}
pub fn setup_entry(pyeval: impl Into<String>) -> Self {
Self::new(pyeval)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
macro_rules! lock_globals {
() => {{
TEST_LOCK
.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|e| e.into_inner())
}};
}
fn reset_pycmd() {
let mut slot = pycmd().lock().unwrap_or_else(|e| e.into_inner());
*slot = None;
}
#[test]
fn get_default_pycmd_returns_python3() {
assert_eq!(get_default_pycmd(), "python3");
}
#[test]
fn set_pycmd_updates_global() {
let _g = lock_globals!();
reset_pycmd();
set_pycmd("py3eval");
let slot = pycmd().lock().unwrap_or_else(|e| e.into_inner());
assert_eq!(slot.as_deref(), Some("py3eval"));
}
#[test]
fn override_from_no_var_leaves_config_untouched() {
let mut cfg = Map::new();
cfg.insert("k".to_string(), Value::from(1));
_override_from(&mut cfg, None, || None);
assert_eq!(cfg.get("k"), Some(&Value::from(1)));
assert_eq!(cfg.len(), 1);
}
#[test]
fn override_from_no_key_merges_full_dict() {
let mut cfg = Map::new();
cfg.insert("k".to_string(), Value::from(1));
_override_from(&mut cfg, None, || {
let mut o = Map::new();
o.insert("k2".to_string(), Value::from(2));
Some(Value::Object(o))
});
assert_eq!(cfg.get("k"), Some(&Value::from(1)));
assert_eq!(cfg.get("k2"), Some(&Value::from(2)));
}
#[test]
fn override_from_with_matching_key_merges_subdict() {
let mut cfg = Map::new();
_override_from(&mut cfg, Some("theme1"), || {
let mut o = Map::new();
let mut inner = Map::new();
inner.insert("seg".to_string(), Value::String("custom".into()));
o.insert("theme1".to_string(), Value::Object(inner));
Some(Value::Object(o))
});
assert_eq!(cfg.get("seg"), Some(&Value::String("custom".into())));
}
#[test]
fn override_from_with_missing_key_no_op() {
let mut cfg = Map::new();
cfg.insert("k".to_string(), Value::from(1));
_override_from(&mut cfg, Some("not_present"), || {
let mut o = Map::new();
o.insert("other_theme".to_string(), Value::from(2));
Some(Value::Object(o))
});
assert_eq!(cfg.get("k"), Some(&Value::from(1)));
assert_eq!(cfg.len(), 1);
}
#[test]
fn create_window_statusline_format_substitutes_idx() {
let f = create_window_statusline_format("PowerlinePyeval");
let s = f(7);
assert_eq!(s, "%!PowerlinePyeval('powerline.statusline(7)')");
}
#[test]
fn create_window_statusline_format_works_for_zero_idx() {
let f = create_window_statusline_format("PowerlinePyeval");
assert_eq!(f(0), "%!PowerlinePyeval('powerline.statusline(0)')");
}
#[test]
fn create_window_statusline_format_with_custom_pyeval() {
let f = create_window_statusline_format("py3eval");
assert_eq!(f(42), "%!py3eval('powerline.statusline(42)')");
}
#[test]
fn get_matcher_module_splits_dotted_name() {
let (m, f) = get_matcher_module("mymodule.fn_name", "vim");
assert_eq!(m, "mymodule");
assert_eq!(f, "fn_name");
}
#[test]
fn get_matcher_module_unrpartitioned_defaults_to_powerline_matchers() {
let (m, f) = get_matcher_module("plain_name", "vim");
assert_eq!(m, "powerline.matchers.vim");
assert_eq!(f, "plain_name");
}
#[test]
fn get_matcher_module_takes_rightmost_dot() {
let (m, f) = get_matcher_module("a.b.c.fn", "vim");
assert_eq!(m, "a.b.c");
assert_eq!(f, "fn");
}
#[test]
fn vim_var_handler_captures_message() {
let mut h = VimVarHandler::new("powerline_log_messages");
h.emit("hello world", None);
assert_eq!(h.captured.len(), 1);
assert_eq!(h.captured[0], "hello world");
}
#[test]
fn vim_var_handler_appends_exc_text_with_newline() {
let mut h = VimVarHandler::new("v");
h.emit("error msg", Some("Traceback (most recent call last):..."));
assert_eq!(
h.captured[0],
"error msg\nTraceback (most recent call last):..."
);
}
#[test]
fn vim_var_handler_stores_varname_as_ascii() {
let h = VimVarHandler::new("powerline_log_messages");
assert_eq!(h.vim_varname, "powerline_log_messages");
}
#[test]
fn vim_powerline_init_defaults_to_powerlinepyeval() {
let p = VimPowerline::default();
assert_eq!(p.pyeval, "PowerlinePyeval");
assert_eq!(p.last_window_id, 1);
}
#[test]
fn vim_powerline_add_local_theme_records_kv_and_returns_true() {
let mut p = VimPowerline::default();
let mut cfg = Map::new();
cfg.insert("seg".to_string(), Value::String("custom".into()));
let r = p.add_local_theme("matcher_a", cfg.clone());
assert!(r);
assert_eq!(p.local_themes.len(), 1);
assert_eq!(p.local_themes[0].0, "matcher_a");
assert_eq!(p.local_themes[0].1.get("seg"), cfg.get("seg"));
}
#[test]
fn failed_unicode_message_includes_window_id() {
assert_eq!(VimPowerline::failed_unicode_message(7), "No window 7");
}
#[test]
fn setup_components_default_emits_statusline_and_tabline() {
let p = VimPowerline::default();
let r = p.setup_components(None);
assert_eq!(r.len(), 2);
assert!(r[0].contains("statusline=%!PowerlinePyeval"));
assert!(r[1].contains("tabline=%!PowerlinePyeval"));
}
#[test]
fn setup_components_only_statusline() {
let p = VimPowerline::default();
let r = p.setup_components(Some(&["statusline"]));
assert_eq!(r.len(), 1);
assert!(r[0].contains("statusline=%!"));
}
#[test]
fn setup_components_only_tabline() {
let p = VimPowerline::default();
let r = p.setup_components(Some(&["tabline"]));
assert_eq!(r.len(), 1);
assert!(r[0].contains("tabline=%!"));
}
#[test]
fn setup_components_unknown_ignored() {
let p = VimPowerline::default();
let r = p.setup_components(Some(&["unknown"]));
assert!(r.is_empty());
}
#[test]
fn setup_components_uses_custom_pyeval() {
let p = VimPowerline::new("py3eval");
let r = p.setup_components(Some(&["statusline"]));
assert!(r[0].contains("py3eval"));
}
#[test]
fn setup_entry_returns_powerline_with_given_pyeval() {
let p = VimPowerline::setup_entry("py3eval");
assert_eq!(p.pyeval, "py3eval");
assert_eq!(p.last_window_id, 1);
}
#[test]
fn reset_highlight_calls_renderer_when_present() {
use std::cell::Cell;
let called = Cell::new(false);
VimPowerline::reset_highlight(Some(|| called.set(true)));
assert!(called.get());
}
#[test]
fn reset_highlight_no_op_when_renderer_missing() {
let f: Option<fn()> = None;
VimPowerline::reset_highlight(f);
}
#[test]
fn assign_window_id_existing_no_conflict_reuses() {
let (id, last, assigned) = VimPowerline::assign_window_id(Some(7), 10, false);
assert_eq!(id, 7);
assert_eq!(last, 10);
assert!(!assigned);
}
#[test]
fn assign_window_id_no_existing_assigns_and_bumps() {
let (id, last, assigned) = VimPowerline::assign_window_id(None, 10, false);
assert_eq!(id, 10);
assert_eq!(last, 11);
assert!(assigned);
}
#[test]
fn assign_window_id_existing_with_conflict_reassigns() {
let (id, last, assigned) = VimPowerline::assign_window_id(Some(5), 10, true);
assert_eq!(id, 10);
assert_eq!(last, 11);
assert!(assigned);
}
#[test]
fn tabline_fallback_window_uses_existing_id_when_set() {
let (window_id, winnr) = VimPowerline::tabline_fallback_window(Some(3), 5, 100);
assert_eq!(window_id, 3);
assert_eq!(winnr, 5);
}
#[test]
fn tabline_fallback_window_defaults_to_last_window_id() {
let (window_id, winnr) = VimPowerline::tabline_fallback_window(None, 5, 100);
assert_eq!(window_id, 100);
assert_eq!(winnr, 5);
}
#[test]
fn do_pyeval_command_builds_return_statement() {
let cmd = VimPowerline::do_pyeval_command("[1, 2, 3]");
assert_eq!(cmd, "return [1, 2, 3]");
}
#[test]
fn do_pyeval_command_handles_dict_json() {
let cmd = VimPowerline::do_pyeval_command(r#"{"key": "value"}"#);
assert_eq!(cmd, r#"return {"key": "value"}"#);
}
#[test]
fn statusline_no_window_message_with_id() {
let msg = VimPowerline::statusline_no_window_message(Some(5)).unwrap();
assert_eq!(msg, "No window 5");
}
#[test]
fn statusline_no_window_message_without_id() {
let msg = VimPowerline::statusline_no_window_message(None).unwrap();
assert_eq!(msg, "No window 0");
}
#[test]
fn get_local_themes_empty_returns_empty() {
let r = VimPowerline::get_local_themes(
&Map::new(),
|_| Some("matcher".to_string()),
|_| Value::Object(Map::new()),
);
assert!(r.is_empty());
}
#[test]
fn get_local_themes_resolves_matchers() {
let mut input = Map::new();
input.insert("help".to_string(), Value::String("help_theme".into()));
input.insert(
"__tabline__".to_string(),
Value::String("tabline_theme".into()),
);
let r = VimPowerline::get_local_themes(
&input,
|k| Some(format!("matchers.vim.{}", k)),
|v| serde_json::json!({"name": v}),
);
assert!(r.contains_key("matchers.vim.help"));
assert_eq!(r["matchers.vim.help"]["config"]["name"], "help_theme");
assert!(r.contains_key("__tabline__"));
assert_eq!(r["__tabline__"]["config"]["name"], "tabline_theme");
}
#[test]
fn get_local_themes_drops_unresolved_matchers() {
let mut input = Map::new();
input.insert("bogus".to_string(), Value::String("v".into()));
let r = VimPowerline::get_local_themes(
&input,
|_| None, |v| serde_json::json!({"name": v}),
);
assert!(r.is_empty());
}
#[test]
fn get_config_paths_returns_vim_var_when_set() {
let paths =
VimPowerline::get_config_paths(Some(vec!["/etc/powerline".to_string()]), || {
vec!["/should/not/use".to_string()]
});
assert_eq!(paths, vec!["/etc/powerline".to_string()]);
}
#[test]
fn get_config_paths_falls_back_to_super_when_unset() {
let paths = VimPowerline::get_config_paths(None, || vec!["/super/path".to_string()]);
assert_eq!(paths, vec!["/super/path".to_string()]);
}
#[test]
fn new_window_returns_win_idx_result() {
let r = VimPowerline::new_window(|| Some((5, 1)));
assert_eq!(r, Some((5, 1)));
}
#[test]
fn new_window_returns_none_when_no_current_window() {
let r = VimPowerline::new_window(|| None);
assert_eq!(r, None);
}
}