#![allow(non_snake_case, non_upper_case_globals, non_camel_case_types)]
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::LazyLock;
use crate::ported::eval::encode::encode_tv2string;
use crate::ported::eval::typval::{tv_get_bool_chk, tv_get_number_chk, tv_get_string_buf_chk};
use crate::ported::eval::typval_defs_h::{
typval_T, typval_vval_union, varnumber_T, BoolVarValue, SpecialVarValue, VarLockStatus, VarType,
};
use crate::ported::message::{emsg, semsg};
pub type OptInt = i64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriState {
kNone = -1,
kFalse = 0,
kTrue = 1,
}
fn TRISTATE_FROM_INT(val: varnumber_T) -> TriState {
if val == 0 {
TriState::kFalse
} else if val >= 1 {
TriState::kTrue
} else {
TriState::kNone
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptValType {
kOptValTypeNil,
kOptValTypeBoolean,
kOptValTypeNumber,
kOptValTypeString,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OptValData {
boolean(TriState),
number(OptInt),
string(String),
nil,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OptVal {
pub r#type: OptValType,
pub data: OptValData,
}
fn NIL_OPTVAL() -> OptVal {
OptVal {
r#type: OptValType::kOptValTypeNil,
data: OptValData::nil,
}
}
fn BOOLEAN_OPTVAL(b: TriState) -> OptVal {
OptVal {
r#type: OptValType::kOptValTypeBoolean,
data: OptValData::boolean(b),
}
}
fn NUMBER_OPTVAL(n: OptInt) -> OptVal {
OptVal {
r#type: OptValType::kOptValTypeNumber,
data: OptValData::number(n),
}
}
fn STRING_OPTVAL(s: String) -> OptVal {
OptVal {
r#type: OptValType::kOptValTypeString,
data: OptValData::string(s),
}
}
const kOptFlagSecure: u32 = 1 << 14;
const kOptFlagFunc: u32 = 1 << 24;
pub const OPT_GLOBAL: i32 = 0x01;
pub const OPT_LOCAL: i32 = 0x02;
pub type OptIndex = usize;
pub const kOptInvalid: OptIndex = usize::MAX;
struct vimoption_T {
fullname: &'static str,
shortname: &'static str,
flags: u32,
r#type: OptValType,
def_val: OptVal,
}
static options: LazyLock<Vec<vimoption_T>> = LazyLock::new(|| {
let b = |fullname: &'static str, shortname: &'static str, def: TriState| vimoption_T {
fullname,
shortname,
flags: 0,
r#type: OptValType::kOptValTypeBoolean,
def_val: BOOLEAN_OPTVAL(def),
};
let n = |fullname: &'static str, shortname: &'static str, def: OptInt| vimoption_T {
fullname,
shortname,
flags: 0,
r#type: OptValType::kOptValTypeNumber,
def_val: NUMBER_OPTVAL(def),
};
let s = |fullname: &'static str, shortname: &'static str, def: &str| vimoption_T {
fullname,
shortname,
flags: 0,
r#type: OptValType::kOptValTypeString,
def_val: STRING_OPTVAL(def.to_string()),
};
vec![
b("ignorecase", "ic", TriState::kFalse),
b("smartcase", "scs", TriState::kFalse),
b("magic", "magic", TriState::kTrue),
b("expandtab", "et", TriState::kFalse),
b("number", "nu", TriState::kFalse),
b("relativenumber", "rnu", TriState::kFalse),
b("wrap", "wrap", TriState::kTrue),
b("hlsearch", "hls", TriState::kFalse),
b("incsearch", "is", TriState::kFalse),
b("autoindent", "ai", TriState::kFalse),
n("tabstop", "ts", 8),
n("shiftwidth", "sw", 8),
n("softtabstop", "sts", 0),
n("textwidth", "tw", 0),
n("scrolloff", "so", 0),
s("filetype", "ft", ""),
s("syntax", "syn", ""),
]
});
thread_local! {
static option_values: RefCell<HashMap<OptIndex, OptVal>> = RefCell::new(HashMap::new());
}
pub fn find_option_len(name: &str, len: usize) -> OptIndex {
let name = &name[..len.min(name.len())];
for (i, opt) in options.iter().enumerate() {
if opt.fullname == name || opt.shortname == name {
return i;
}
}
kOptInvalid
}
pub fn find_option(name: &str) -> OptIndex {
find_option_len(name, name.len())
}
pub fn find_option_end(arg: &str, opt_idxp: &mut OptIndex) -> Option<usize> {
let p = arg.as_bytes();
if p.is_empty() || !p[0].is_ascii_alphabetic() {
*opt_idxp = kOptInvalid;
return None;
}
let mut i = 0;
while i < p.len() && p[i].is_ascii_alphabetic() {
i += 1;
}
*opt_idxp = find_option_len(arg, i);
Some(i)
}
pub fn option_has_type(opt_idx: OptIndex, r#type: OptValType) -> bool {
opt_idx != kOptInvalid && options[opt_idx].r#type == r#type
}
pub fn is_tty_option(name: &str) -> bool {
name == "term" || name == "ttytype" || name.starts_with("t_")
}
pub fn optval_free(o: OptVal) {
let _ = o;
}
pub fn optval_copy(o: OptVal) -> OptVal {
match o.r#type {
OptValType::kOptValTypeNil
| OptValType::kOptValTypeBoolean
| OptValType::kOptValTypeNumber => o,
OptValType::kOptValTypeString => {
let OptValData::string(ref str_) = o.data else {
return o;
};
STRING_OPTVAL(str_.clone())
}
}
}
pub fn optval_from_varp(opt_idx: OptIndex) -> OptVal {
let r#type = options[opt_idx].r#type;
let stored = option_values.with(|m| m.borrow().get(&opt_idx).cloned());
match r#type {
OptValType::kOptValTypeNil => NIL_OPTVAL(),
OptValType::kOptValTypeBoolean
| OptValType::kOptValTypeNumber
| OptValType::kOptValTypeString => stored.unwrap_or_else(|| options[opt_idx].def_val.clone()),
}
}
pub fn get_option_value(opt_idx: OptIndex, opt_flags: i32) -> OptVal {
let _ = opt_flags;
if opt_idx == kOptInvalid {
return NIL_OPTVAL();
}
optval_copy(optval_from_varp(opt_idx))
}
pub fn set_option_value(opt_idx: OptIndex, value: OptVal, opt_flags: i32) -> Option<String> {
assert!(opt_idx != kOptInvalid);
let _ = opt_flags;
let flags = options[opt_idx].flags;
let _ = (flags, kOptFlagSecure);
option_values.with(|m| {
m.borrow_mut().insert(opt_idx, optval_copy(value));
});
None
}
pub fn set_option_value_handle_tty(
name: &str,
opt_idx: OptIndex,
value: OptVal,
opt_flags: i32,
) -> Option<String> {
if opt_idx == kOptInvalid {
if is_tty_option(name) {
return None;
}
return Some(format!("E355: Unknown option: {name}")); }
set_option_value(opt_idx, value, opt_flags)
}
fn tv_to_optval(tv: &typval_T, opt_idx: OptIndex, option: &str, error: &mut bool) -> OptVal {
let mut value = NIL_OPTVAL();
let mut err = false;
let is_tty_opt = is_tty_option(option);
let option_has_bool = !is_tty_opt && option_has_type(opt_idx, OptValType::kOptValTypeBoolean);
let option_has_num = !is_tty_opt && option_has_type(opt_idx, OptValType::kOptValTypeNumber);
let option_has_str = is_tty_opt || option_has_type(opt_idx, OptValType::kOptValTypeString);
let tv_is_func = matches!(tv.v_type, VarType::VAR_FUNC | VarType::VAR_PARTIAL);
if !is_tty_opt && (options[opt_idx].flags & kOptFlagFunc) != 0 && tv_is_func {
let strval = encode_tv2string(tv);
value = STRING_OPTVAL(strval);
} else if option_has_bool || option_has_num {
let n = if option_has_num {
tv_get_number_chk(tv, Some(&mut err))
} else {
tv_get_bool_chk(tv, Some(&mut err))
};
if !err && tv.v_type == VarType::VAR_STRING && n == 0 {
let sbytes: &[u8] = match &tv.vval {
typval_vval_union::v_string(sv) => sv.as_bytes(),
_ => &[],
};
let mut idx = 0usize;
while idx < sbytes.len() && sbytes[idx] == b'0' {
idx += 1;
}
if idx == 0 || idx < sbytes.len() {
err = true;
let sstr = match &tv.vval {
typval_vval_union::v_string(sv) => sv.as_str(),
_ => "",
};
semsg(&format!("E521: Number required: &{option} = '{sstr}'"));
}
}
value = if option_has_num {
NUMBER_OPTVAL(n as OptInt)
} else {
BOOLEAN_OPTVAL(TRISTATE_FROM_INT(n))
};
} else if option_has_str {
if tv.v_type != VarType::VAR_BOOL && tv.v_type != VarType::VAR_SPECIAL {
let strval = tv_get_string_buf_chk(tv);
err = strval.is_none();
value = STRING_OPTVAL(strval.unwrap_or_default());
} else if !is_tty_opt {
err = true;
emsg("E928: String required"); }
} else {
unreachable!("tv_to_optval: option has no known type");
}
*error = err;
value
}
pub fn optval_as_tv(value: OptVal, numbool: bool) -> typval_T {
let mut rettv = typval_T {
v_type: VarType::VAR_SPECIAL,
v_lock: VarLockStatus::VAR_UNLOCKED,
vval: typval_vval_union::v_special(SpecialVarValue::kSpecialVarNull),
};
match value.r#type {
OptValType::kOptValTypeNil => {}
OptValType::kOptValTypeBoolean => {
let boolean = match value.data {
OptValData::boolean(t) => t,
_ => TriState::kNone,
};
if numbool {
rettv.v_type = VarType::VAR_NUMBER;
rettv.vval = typval_vval_union::v_number(boolean as varnumber_T);
} else if boolean != TriState::kNone {
rettv.v_type = VarType::VAR_BOOL;
rettv.vval = typval_vval_union::v_bool(if boolean == TriState::kTrue {
BoolVarValue::kBoolVarTrue
} else {
BoolVarValue::kBoolVarFalse
});
}
}
OptValType::kOptValTypeNumber => {
let number = match value.data {
OptValData::number(n) => n,
_ => 0,
};
rettv.v_type = VarType::VAR_NUMBER;
rettv.vval = typval_vval_union::v_number(number as varnumber_T);
}
OptValType::kOptValTypeString => {
let string = match value.data {
OptValData::string(s) => s,
_ => String::new(),
};
rettv.v_type = VarType::VAR_STRING;
rettv.vval = typval_vval_union::v_string(string);
}
}
rettv
}
pub fn set_option_from_tv(varname: &str, varp: &typval_T) {
let opt_idx = find_option(varname);
if opt_idx == kOptInvalid {
semsg(&format!("E355: Unknown option: {varname}")); return;
}
let mut error = false;
let value = tv_to_optval(varp, opt_idx, varname, &mut error);
if !error {
let errmsg = set_option_value_handle_tty(varname, opt_idx, value.clone(), OPT_LOCAL);
if let Some(errmsg) = errmsg {
emsg(&errmsg);
}
}
optval_free(value);
}
pub fn find_option_var_end(arg: &str) -> (Option<String>, OptIndex, i32) {
let bytes = arg.as_bytes();
let mut p = 1usize;
let opt_flags;
if p + 1 < bytes.len() && bytes[p] == b'g' && bytes[p + 1] == b':' {
opt_flags = OPT_GLOBAL;
p += 2;
} else if p + 1 < bytes.len() && bytes[p] == b'l' && bytes[p + 1] == b':' {
opt_flags = OPT_LOCAL;
p += 2;
} else {
opt_flags = 0;
}
let mut opt_idx = kOptInvalid;
let end = find_option_end(&arg[p..], &mut opt_idx);
match end {
Some(len) => (Some(arg[p..p + len].to_string()), opt_idx, opt_flags),
None => (None, opt_idx, opt_flags),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn reset_store() {
option_values.with(|m| m.borrow_mut().clear());
}
#[test]
fn find_option_resolves_name_and_abbrev() {
assert_eq!(find_option("ignorecase"), find_option("ic"));
assert_ne!(find_option("ignorecase"), kOptInvalid);
assert_eq!(find_option("nosuchopt"), kOptInvalid);
}
#[test]
fn get_default_and_set_number() {
reset_store();
let ts = find_option("tabstop");
let v = get_option_value(ts, 0);
assert_eq!(v.data, OptValData::number(8));
assert_eq!(set_option_value(ts, NUMBER_OPTVAL(4), 0), None);
assert_eq!(get_option_value(ts, 0).data, OptValData::number(4));
}
#[test]
fn tv_to_optval_number_and_bool_and_string() {
reset_store();
let ts = find_option("tabstop");
let mut err = false;
let ov = tv_to_optval(&typval_T::from(2 as varnumber_T), ts, "tabstop", &mut err);
assert!(!err);
assert_eq!(ov.data, OptValData::number(2));
let ic = find_option("ignorecase");
let ov = tv_to_optval(
&typval_T::from(1 as varnumber_T),
ic,
"ignorecase",
&mut err,
);
assert!(!err);
assert_eq!(ov.data, OptValData::boolean(TriState::kTrue));
let ft = find_option("filetype");
let ov = tv_to_optval(
&typval_T::from("rust".to_string()),
ft,
"filetype",
&mut err,
);
assert!(!err);
assert_eq!(ov.data, OptValData::string("rust".to_string()));
}
#[test]
fn tv_to_optval_e521_on_nonnumeric_string() {
let ts = find_option("tabstop");
let mut err = false;
let _ = tv_to_optval(&typval_T::from("abc".to_string()), ts, "tabstop", &mut err);
assert!(err);
}
#[test]
fn optval_as_tv_bool_numbool_and_native() {
let tv = optval_as_tv(BOOLEAN_OPTVAL(TriState::kTrue), true);
assert_eq!(tv.v_type, VarType::VAR_NUMBER);
assert!(matches!(tv.vval, typval_vval_union::v_number(1)));
let tv = optval_as_tv(BOOLEAN_OPTVAL(TriState::kTrue), false);
assert_eq!(tv.v_type, VarType::VAR_BOOL);
assert!(matches!(
tv.vval,
typval_vval_union::v_bool(BoolVarValue::kBoolVarTrue)
));
let tv = optval_as_tv(BOOLEAN_OPTVAL(TriState::kNone), false);
assert_eq!(tv.v_type, VarType::VAR_SPECIAL);
}
#[test]
fn set_option_from_tv_roundtrip_and_unknown() {
reset_store();
set_option_from_tv("shiftwidth", &typval_T::from(3 as varnumber_T));
let sw = find_option("shiftwidth");
let tv = optval_as_tv(get_option_value(sw, 0), true);
assert!(matches!(tv.vval, typval_vval_union::v_number(3)));
assert_eq!(find_option("definitelynotanoption"), kOptInvalid);
}
#[test]
fn find_option_var_end_decodes_scope_prefix() {
let (name, idx, flags) = find_option_var_end("&ic");
assert_eq!(name.as_deref(), Some("ic"));
assert_ne!(idx, kOptInvalid);
assert_eq!(flags, 0);
let (name, _idx, flags) = find_option_var_end("&g:number");
assert_eq!(name.as_deref(), Some("number"));
assert_eq!(flags, OPT_GLOBAL);
let (name, _idx, flags) = find_option_var_end("&l:wrap");
assert_eq!(name.as_deref(), Some("wrap"));
assert_eq!(flags, OPT_LOCAL);
}
}