use crate::ported::ipython::{IPythonPowerline, RewriteResult};
use regex::Regex;
use serde_json::{Map, Value};
use std::sync::OnceLock;
pub fn rspace() -> &'static Regex {
static R: OnceLock<Regex> = OnceLock::new();
R.get_or_init(|| Regex::new(r"(\s*)$").unwrap())
}
#[derive(Debug, Clone, Default)]
pub struct PromptCache {
pub prompt_count: u64,
pub last_prompt: String,
}
#[derive(Debug, Clone)]
pub struct IPythonInfo {
pub prompt_count: u64,
}
impl IPythonInfo {
pub fn new(prompt_count: u64) -> Self {
Self { prompt_count }
}
pub fn prompt_count(&self) -> u64 {
self.prompt_count
}
}
#[derive(Debug, Clone)]
pub struct PromptStrings {
pub p_str: String,
pub p_str_nocolor: String,
pub powerline_prompt_width: usize,
}
pub struct PowerlinePrompt {
pub segment_info: IPythonInfo,
pub cache: PromptCache,
pub sep: Option<String>,
pub pad_left: bool,
pub p_str: String,
pub p_str_nocolor: String,
pub powerline_prompt_width: usize,
}
impl PowerlinePrompt {
pub fn new(cache: PromptCache, sep: Option<String>) -> Self {
Self {
segment_info: IPythonInfo::new(cache.prompt_count),
cache,
sep,
pad_left: false,
p_str: String::new(),
p_str_nocolor: String::new(),
powerline_prompt_width: 0,
}
}
pub fn set_p_str<R>(&mut self, is_prompt: bool, prompt_type: &str, mut render: R)
where
R: FnMut(bool, &str, &str) -> PromptStrings,
{
let r = render(is_prompt, "left", prompt_type);
self.p_str = r.p_str;
self.p_str_nocolor = r.p_str_nocolor;
self.powerline_prompt_width = r.powerline_prompt_width;
}
pub fn set_colors() {
}
}
pub struct PowerlinePrompt1 {
pub base: PowerlinePrompt,
pub nrspaces: usize,
pub last_in_nrspaces: usize,
}
impl PowerlinePrompt1 {
pub const PROMPT_TYPE: &'static str = "in";
pub const IS_PROMPT: bool = true;
pub fn new(cache: PromptCache, sep: Option<String>) -> Self {
Self {
base: PowerlinePrompt::new(cache, sep),
nrspaces: 0,
last_in_nrspaces: 0,
}
}
pub fn to_p_str<R>(&mut self, render: R) -> String
where
R: FnMut(bool, &str, &str) -> PromptStrings,
{
self.base.cache.prompt_count += 1;
self.set_p_str(render);
let last_line = self
.base
.p_str_nocolor
.rsplit('\n')
.next()
.unwrap_or("")
.to_string();
self.base.cache.last_prompt = last_line;
self.base.p_str.clone()
}
pub fn set_p_str<R>(&mut self, render: R)
where
R: FnMut(bool, &str, &str) -> PromptStrings,
{
self.base
.set_p_str(Self::IS_PROMPT, Self::PROMPT_TYPE, render);
let n = rspace()
.find(&self.base.p_str_nocolor)
.map(|m| m.len())
.unwrap_or(0);
self.nrspaces = n;
self.last_in_nrspaces = n;
}
pub fn auto_rewrite<R>(&self, mut render: R) -> RewriteResult
where
R: FnMut(bool, &str, &str) -> String,
{
let rendered = render(false, "left", "rewrite");
let padding = " ".repeat(self.nrspaces);
RewriteResult::new(format!("{}{}", rendered, padding))
}
}
pub struct PowerlinePromptOut {
pub base: PowerlinePrompt,
pub last_in_nrspaces: usize,
}
impl PowerlinePromptOut {
pub const PROMPT_TYPE: &'static str = "out";
pub const IS_PROMPT: bool = false;
pub fn new(cache: PromptCache, sep: Option<String>, last_in_nrspaces: usize) -> Self {
Self {
base: PowerlinePrompt::new(cache, sep),
last_in_nrspaces,
}
}
pub fn set_p_str<R>(&mut self, render: R)
where
R: FnMut(bool, &str, &str) -> PromptStrings,
{
self.base
.set_p_str(Self::IS_PROMPT, Self::PROMPT_TYPE, render);
let spaces = " ".repeat(self.last_in_nrspaces);
self.base.p_str.push_str(&spaces);
self.base.p_str_nocolor.push_str(&spaces);
}
}
pub struct PowerlinePrompt2 {
pub out_base: PowerlinePromptOut,
}
impl PowerlinePrompt2 {
pub const PROMPT_TYPE: &'static str = "in2";
pub const IS_PROMPT: bool = true;
pub fn new(cache: PromptCache, sep: Option<String>, last_in_nrspaces: usize) -> Self {
Self {
out_base: PowerlinePromptOut::new(cache, sep, last_in_nrspaces),
}
}
pub fn set_p_str<R>(&mut self, render: R)
where
R: FnMut(bool, &str, &str) -> PromptStrings,
{
self.out_base
.base
.set_p_str(Self::IS_PROMPT, Self::PROMPT_TYPE, render);
let spaces = " ".repeat(self.out_base.last_in_nrspaces);
self.out_base.base.p_str.push_str(&spaces);
self.out_base.base.p_str_nocolor.push_str(&spaces);
}
}
pub struct ConfigurableIPythonPowerline {
pub base: IPythonPowerline,
pub setup_done: bool,
}
impl Default for ConfigurableIPythonPowerline {
fn default() -> Self {
Self::new()
}
}
impl ConfigurableIPythonPowerline {
pub fn new() -> Self {
Self {
base: IPythonPowerline::new(),
setup_done: false,
}
}
pub fn init(
&mut self,
config_overrides: Option<Map<String, Value>>,
theme_overrides: Map<String, Value>,
config_paths: Vec<String>,
) -> &'static str {
self.base.config_overrides = config_overrides;
self.base.theme_overrides = theme_overrides;
self.base.config_paths = config_paths;
".pre_5"
}
pub fn ipython_magic(&self, parameter_s: &str) -> Result<(), String> {
if parameter_s == "reload" {
Ok(())
} else {
Err(format!("Expected `reload`, but got {}", parameter_s))
}
}
pub fn do_setup(
&mut self,
outputcache: &mut Map<String, Value>,
shutdown_hook: &mut ShutdownHook,
) {
for attr in ["prompt1", "prompt2", "prompt_out"] {
outputcache.insert(
attr.to_string(),
Value::String(format!("<PowerlinePrompt:{}>", attr)),
);
}
shutdown_hook.set_powerline();
self.setup_done = true;
}
}
pub struct ShutdownHook {
pub powerline_live: bool,
pub call_count: u32,
}
impl Default for ShutdownHook {
fn default() -> Self {
Self::new()
}
}
impl ShutdownHook {
pub fn new() -> Self {
Self {
powerline_live: false,
call_count: 0,
}
}
pub fn set_powerline(&mut self) {
self.powerline_live = true;
}
pub fn call(&mut self) -> &'static str {
if self.powerline_live {
self.call_count += 1;
}
"TryNext"
}
}
pub fn late_startup_hook<F>(setup_powerline: F) -> Result<(), &'static str>
where
F: FnOnce(),
{
setup_powerline();
Err("TryNext")
}
pub fn setup() -> (ConfigurableIPythonPowerline, ShutdownHook) {
let powerline = ConfigurableIPythonPowerline::new();
let shutdown_hook = ShutdownHook::new();
(powerline, shutdown_hook)
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_render(_is_prompt: bool, _side: &str, _matcher: &str) -> PromptStrings {
PromptStrings {
p_str: "PROMPT ".to_string(),
p_str_nocolor: "PROMPT ".to_string(),
powerline_prompt_width: 7,
}
}
#[test]
fn rspace_matches_trailing_whitespace() {
let m = rspace().find("hello ").unwrap();
assert_eq!(m.as_str(), " ");
}
#[test]
fn rspace_matches_empty_at_end_of_no_trailing_space() {
let m = rspace().find("hello").unwrap();
assert_eq!(m.as_str(), "");
}
#[test]
fn ipython_info_prompt_count_returns_value() {
let i = IPythonInfo::new(42);
assert_eq!(i.prompt_count(), 42);
}
#[test]
fn powerline_prompt_set_p_str_stashes_render_result() {
let mut p = PowerlinePrompt::new(PromptCache::default(), None);
p.set_p_str(true, "in", fake_render);
assert_eq!(p.p_str, "PROMPT ");
assert_eq!(p.p_str_nocolor, "PROMPT ");
assert_eq!(p.powerline_prompt_width, 7);
}
#[test]
fn powerline_prompt_set_colors_is_noop() {
PowerlinePrompt::set_colors();
}
#[test]
fn powerline_prompt_init_sets_pad_left_false() {
let p = PowerlinePrompt::new(PromptCache::default(), None);
assert!(!p.pad_left);
assert!(p.sep.is_none());
}
#[test]
fn powerline_prompt_init_preserves_sep_when_supplied() {
let p = PowerlinePrompt::new(PromptCache::default(), Some(" | ".to_string()));
assert_eq!(p.sep, Some(" | ".to_string()));
}
#[test]
fn powerline_prompt1_advances_count_and_snapshots_last_prompt() {
let cache = PromptCache {
prompt_count: 5,
..Default::default()
};
let mut p = PowerlinePrompt1::new(cache, None);
let _ = p.to_p_str(|_is, _side, _m| PromptStrings {
p_str: "line1\nIN> ".to_string(),
p_str_nocolor: "line1\nIN> ".to_string(),
powerline_prompt_width: 4,
});
assert_eq!(p.base.cache.prompt_count, 6);
assert_eq!(p.base.cache.last_prompt, "IN> ");
}
#[test]
fn powerline_prompt1_set_p_str_counts_trailing_spaces() {
let mut p = PowerlinePrompt1::new(PromptCache::default(), None);
p.set_p_str(|_is, _side, _m| PromptStrings {
p_str: "IN> ".to_string(),
p_str_nocolor: "IN> ".to_string(),
powerline_prompt_width: 8,
});
assert_eq!(p.nrspaces, 5);
assert_eq!(p.last_in_nrspaces, 5);
}
#[test]
fn powerline_prompt1_set_p_str_zero_trailing_spaces() {
let mut p = PowerlinePrompt1::new(PromptCache::default(), None);
p.set_p_str(|_is, _side, _m| PromptStrings {
p_str: "IN>".to_string(),
p_str_nocolor: "IN>".to_string(),
powerline_prompt_width: 3,
});
assert_eq!(p.nrspaces, 0);
}
#[test]
fn powerline_prompt1_auto_rewrite_pads_with_nrspaces() {
let mut p = PowerlinePrompt1::new(PromptCache::default(), None);
p.nrspaces = 3;
let r = p.auto_rewrite(|_is, _side, _m| "REWRITE".to_string());
assert_eq!(r.prompt, "REWRITE ");
}
#[test]
fn powerline_prompt_out_pads_with_last_in_nrspaces() {
let mut out = PowerlinePromptOut::new(PromptCache::default(), None, 4);
out.set_p_str(fake_render);
assert_eq!(out.base.p_str, "PROMPT "); assert_eq!(out.base.p_str_nocolor, "PROMPT ");
}
#[test]
fn powerline_prompt2_inherits_padding_behaviour() {
let mut p2 = PowerlinePrompt2::new(PromptCache::default(), None, 2);
let mut last_matcher = String::new();
p2.set_p_str(|_is, _side, matcher| {
last_matcher = matcher.to_string();
PromptStrings {
p_str: "IN2>".to_string(),
p_str_nocolor: "IN2>".to_string(),
powerline_prompt_width: 4,
}
});
assert_eq!(last_matcher, "in2");
assert_eq!(p2.out_base.base.p_str, "IN2> "); }
#[test]
fn powerline_prompt2_is_prompt_true() {
const _: () = assert!(PowerlinePrompt2::IS_PROMPT);
assert_eq!(PowerlinePrompt2::PROMPT_TYPE, "in2");
}
#[test]
fn powerline_prompt_out_is_not_a_prompt() {
const _: () = assert!(!PowerlinePromptOut::IS_PROMPT);
assert_eq!(PowerlinePromptOut::PROMPT_TYPE, "out");
}
#[test]
fn configurable_init_pins_pre_5_renderer() {
let mut c = ConfigurableIPythonPowerline::new();
let r = c.init(None, Map::new(), Vec::new());
assert_eq!(r, ".pre_5");
}
#[test]
fn configurable_init_stashes_all_three_overrides() {
let mut c = ConfigurableIPythonPowerline::new();
let mut overrides = Map::new();
overrides.insert("k".to_string(), Value::from(1));
let mut themes = Map::new();
themes.insert("t".to_string(), Value::from(2));
c.init(Some(overrides), themes, vec!["/p".to_string()]);
assert!(c.base.config_overrides.is_some());
assert_eq!(c.base.theme_overrides.len(), 1);
assert_eq!(c.base.config_paths, vec!["/p".to_string()]);
}
#[test]
fn ipython_magic_reload_succeeds() {
let c = ConfigurableIPythonPowerline::new();
assert!(c.ipython_magic("reload").is_ok());
}
#[test]
fn ipython_magic_other_errors() {
let c = ConfigurableIPythonPowerline::new();
let r = c.ipython_magic("foo");
assert!(r.is_err());
assert!(r.unwrap_err().contains("Expected `reload`"));
}
#[test]
fn do_setup_installs_three_prompts_and_wires_hook() {
let mut c = ConfigurableIPythonPowerline::new();
let mut outputcache = Map::new();
let mut hook = ShutdownHook::new();
c.do_setup(&mut outputcache, &mut hook);
for key in ["prompt1", "prompt2", "prompt_out"] {
assert!(outputcache.contains_key(key));
}
assert!(hook.powerline_live);
assert!(c.setup_done);
}
#[test]
fn shutdown_hook_call_returns_try_next() {
let mut h = ShutdownHook::new();
h.set_powerline();
assert_eq!(h.call(), "TryNext");
assert_eq!(h.call_count, 1);
}
#[test]
fn shutdown_hook_call_skips_when_target_dead() {
let mut h = ShutdownHook::new();
h.call();
assert_eq!(h.call_count, 0);
}
#[test]
fn setup_returns_pair_of_powerline_and_hook() {
let (powerline, hook) = setup();
assert!(!powerline.setup_done);
assert!(!hook.powerline_live);
}
#[test]
fn late_startup_hook_dispatches_setup_then_returns_try_next() {
let invoked = std::cell::Cell::new(false);
let r = late_startup_hook(|| invoked.set(true));
assert!(invoked.get());
assert_eq!(r, Err("TryNext"));
}
}