use crate::ported::eval::typval::tv_get_string;
use crate::ported::eval::typval_defs_h::{typval_T, typval_vval_union::v_number, varnumber_T};
use std::cell::RefCell;
#[derive(Debug, Clone)]
pub enum AdviceKind {
Before,
After,
Around,
}
#[derive(Debug, Clone)]
pub struct Intercept {
pub pattern: String,
pub kind: AdviceKind,
pub code: String,
pub id: u32,
}
thread_local! {
static INTERCEPTS: RefCell<Vec<Intercept>> = const { RefCell::new(Vec::new()) };
}
pub(crate) fn intercept_matches(pattern: &str, cmd_name: &str, full_cmd: &str) -> bool {
if pattern == "*" || pattern == "all" {
return true;
}
if pattern == cmd_name {
return true;
}
if pattern.contains('*') || pattern.contains('?') {
if let Ok(pat) = glob::Pattern::new(pattern) {
return pat.matches(cmd_name) || pat.matches(full_cmd);
}
}
false
}
pub(crate) fn matching(cmd_name: &str, full_cmd: &str) -> Vec<Intercept> {
INTERCEPTS.with(|v| {
v.borrow()
.iter()
.filter(|i| intercept_matches(&i.pattern, cmd_name, full_cmd))
.cloned()
.collect()
})
}
pub(crate) fn is_empty() -> bool {
INTERCEPTS.with(|v| v.borrow().is_empty())
}
pub fn register(kind: AdviceKind, pattern: String, code: String) -> u32 {
INTERCEPTS.with(|v| {
let mut v = v.borrow_mut();
let id = v.iter().map(|i| i.id).max().unwrap_or(0) + 1;
v.push(Intercept {
pattern,
kind,
code,
id,
});
id
})
}
fn kind_str(k: &AdviceKind) -> &'static str {
match k {
AdviceKind::Before => "before",
AdviceKind::After => "after",
AdviceKind::Around => "around",
}
}
pub fn list() -> i32 {
INTERCEPTS.with(|v| {
let v = v.borrow();
if v.is_empty() {
println!("no intercepts registered");
return 0;
}
let bold = |s: &str| format!("\x1b[1m{s}\x1b[0m");
let cyan = |s: &str| format!("\x1b[36m{s}\x1b[0m");
println!(
"{:>4} {:<8} {:<20} {}",
bold("ID"),
bold("KIND"),
bold("PATTERN"),
bold("CODE")
);
for i in v.iter() {
let code_preview = if i.code.len() > 40 {
format!("{}...", &i.code[..37])
} else {
i.code.clone()
};
println!(
"{:>4} {:<8} {:<20} {}",
cyan(&i.id.to_string()),
kind_str(&i.kind),
i.pattern,
code_preview
);
}
0
})
}
pub fn remove(id: u32) -> i32 {
INTERCEPTS.with(|v| {
let mut v = v.borrow_mut();
let before = v.len();
v.retain(|i| i.id != id);
if v.len() < before {
println!("removed intercept {id}");
0
} else {
eprintln!("vimlrs:Intercept:1: no intercept with ID {id}");
1
}
})
}
pub fn clear() -> i32 {
INTERCEPTS.with(|v| {
let mut v = v.borrow_mut();
let count = v.len();
v.clear();
println!("cleared {count} intercepts");
0
})
}
pub fn f_intercept(argvars: &[typval_T], rettv: &mut typval_T) {
let kind = match tv_get_string(&argvars[0]).as_str() {
"before" => AdviceKind::Before,
"after" => AdviceKind::After,
"around" => AdviceKind::Around,
other => {
crate::ported::message::semsg(&format!(
"E475: intercept(): kind must be before|after|around, got '{other}'"
));
rettv.vval = v_number(-1);
return;
}
};
let id = register(kind, tv_get_string(&argvars[1]), tv_get_string(&argvars[2]));
rettv.vval = v_number(id as varnumber_T);
}
pub fn f_intercept_proceed(_argvars: &[typval_T], rettv: &mut typval_T) {
*rettv = crate::fusevm_bridge::intercept_proceed();
}
pub fn ex_intercept(args: &str) -> i32 {
use crate::ported::message::{emsg, semsg};
let args = args.trim();
if args.is_empty() {
println!("Usage: :Intercept <before|after|around> <pattern> {{ code }}");
println!(" :Intercept list | remove <id> | clear | proceed");
return 0;
}
let mut it = args.splitn(2, char::is_whitespace);
let sub = it.next().unwrap_or("");
let rest = it.next().unwrap_or("").trim();
match sub {
"list" => list(),
"clear" => clear(),
"proceed" => {
crate::fusevm_bridge::intercept_proceed();
0
}
"remove" => match rest
.split_whitespace()
.next()
.and_then(|s| s.parse::<u32>().ok())
{
Some(id) => remove(id),
None => {
emsg("E474: :Intercept remove requires a numeric ID");
1
}
},
"before" | "after" | "around" => {
let kind = match sub {
"before" => AdviceKind::Before,
"after" => AdviceKind::After,
_ => AdviceKind::Around,
};
let mut parts = rest.splitn(2, char::is_whitespace);
let pattern = parts.next().unwrap_or("").to_string();
let mut code = parts.next().unwrap_or("").trim().to_string();
if pattern.is_empty() || code.is_empty() {
semsg(&format!(
"E474: :Intercept {sub} requires <pattern> {{ code }}"
));
return 1;
}
if code.starts_with('{') && code.ends_with('}') {
code = code[1..code.len() - 1].trim().to_string();
}
let id = register(kind, pattern, code);
println!("intercept #{id} registered");
0
}
_ => {
semsg(&format!("E474: :Intercept: unknown sub-command '{sub}'"));
1
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn star_matches_anything() {
assert!(intercept_matches("*", "anything", "anything --here"));
assert!(intercept_matches("*", "", ""));
}
#[test]
fn all_matches_anything() {
assert!(intercept_matches("all", "Ls", "Ls -la"));
assert!(intercept_matches("all", "Git", "Git status"));
}
#[test]
fn exact_match_on_cmd_name() {
assert!(intercept_matches("Git", "Git", "Git push"));
assert!(intercept_matches("Ls", "Ls", "Ls -la"));
}
#[test]
fn exact_pattern_does_not_match_different_name() {
assert!(!intercept_matches("Git", "Svn", "Svn diff"));
assert!(!intercept_matches("Ls", "Lsof", "Lsof -p 1"));
}
#[test]
fn glob_star_matches_prefix() {
assert!(intercept_matches("Git *", "Git", "Git push origin"));
}
#[test]
fn glob_star_underscore_prefix_matches_completion_funcs() {
assert!(intercept_matches("_*", "_files", "_files"));
assert!(intercept_matches("_*", "_describe", "_describe"));
}
#[test]
fn glob_star_does_not_match_non_prefix() {
assert!(!intercept_matches("_*", "files", "files"));
}
#[test]
fn question_mark_glob_matches_single_char() {
assert!(intercept_matches("L?", "Ls", "Ls"));
assert!(!intercept_matches("L?", "Lsof", "Lsof"));
}
#[test]
fn unmatched_pattern_without_glob_chars_returns_false() {
assert!(!intercept_matches("nope", "Git", "Git push"));
}
#[test]
fn invalid_glob_pattern_returns_false() {
assert!(!intercept_matches("[invalid", "Git", "Git push"));
}
#[test]
fn empty_pattern_does_not_match_non_empty_cmd() {
assert!(!intercept_matches("", "Ls", "Ls -la"));
}
#[test]
fn empty_pattern_matches_empty_cmd_exactly() {
assert!(intercept_matches("", "", ""));
}
#[test]
fn advice_kind_variants_round_trip_clone() {
let b = AdviceKind::Before;
let a = AdviceKind::After;
let r = AdviceKind::Around;
assert!(matches!(b.clone(), AdviceKind::Before));
assert!(matches!(a.clone(), AdviceKind::After));
assert!(matches!(r.clone(), AdviceKind::Around));
}
#[test]
fn intercept_struct_clone_preserves_fields() {
let i = Intercept {
pattern: "Git *".into(),
kind: AdviceKind::Before,
code: "echo 'before'".into(),
id: 42,
};
let c = i.clone();
assert_eq!(c.pattern, "Git *");
assert!(matches!(c.kind, AdviceKind::Before));
assert_eq!(c.code, "echo 'before'");
assert_eq!(c.id, 42);
}
#[test]
fn register_allocates_increasing_ids_then_remove_clear() {
clear();
let id1 = register(AdviceKind::Before, "Foo".into(), "echo 1".into());
let id2 = register(AdviceKind::After, "Foo".into(), "echo 2".into());
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert!(!is_empty());
assert_eq!(matching("Foo", "Foo").len(), 2);
assert_eq!(matching("Bar", "Bar").len(), 0);
assert_eq!(remove(id1), 0);
assert_eq!(matching("Foo", "Foo").len(), 1);
assert_eq!(remove(999), 1); clear();
assert!(is_empty());
}
}