#![allow(non_snake_case)]
use crate::ported::params::{gethparam, getsparam};
use std::collections::HashMap;
use std::sync::Mutex;
const DEFAULT_PAIRS: &[(char, char)] = &[
('`', '`'),
('\'', '\''),
('"', '"'),
('{', '}'),
('[', ']'),
('(', ')'),
(' ', ' '),
];
fn default_lbound(group: &str) -> Option<&'static str> {
match group {
"all" => Some(r"[.:/!]"),
"quotes" => Some(r"[\]})a-zA-Z0-9]"),
"spaces" => Some(r"[^{(\[]"),
"braces" => Some(""),
"`" => Some("`"),
"\"" => Some("\""),
"'" => Some("'"),
_ => None,
}
}
fn default_rbound(group: &str) -> Option<&'static str> {
match group {
"all" => Some(r"[\[{(<,.:?/%$!a-zA-Z0-9]"),
"quotes" => Some("[a-zA-Z0-9]"),
"spaces" => Some(r"[^\]})]"),
"braces" => Some(""),
_ => None,
}
}
fn zsh_ere_to_rust(pat: &str) -> String {
let mut out = String::with_capacity(pat.len() + 4);
let mut chars = pat.chars().peekable();
let mut in_class = false;
let mut class_start = false;
while let Some(c) = chars.next() {
match c {
'\\' => {
out.push(c);
if let Some(n) = chars.next() {
out.push(n);
}
class_start = false;
}
'[' if !in_class => {
in_class = true;
out.push('[');
if chars.peek() == Some(&'^') {
out.push(chars.next().unwrap());
}
class_start = true;
}
'[' if in_class => {
out.push_str(r"\[");
class_start = false;
}
']' if in_class && class_start => {
out.push_str(r"\]"); class_start = false;
}
']' if in_class => {
in_class = false;
out.push(']');
}
_ => {
out.push(c);
class_start = false;
}
}
}
out
}
pub struct AutopairConfig {
pub pairs: Vec<(char, char)>,
pub between_whitespace: bool,
lbounds: HashMap<String, String>,
rbounds: HashMap<String, String>,
}
impl Default for AutopairConfig {
fn default() -> Self {
Self {
pairs: DEFAULT_PAIRS.to_vec(),
between_whitespace: false,
lbounds: HashMap::new(),
rbounds: HashMap::new(),
}
}
}
impl AutopairConfig {
pub fn from_params() -> Self {
let mut cfg = Self::default();
if let Some(flat) = gethparam("AUTOPAIR_PAIRS") {
let pairs: Vec<(char, char)> = flat
.chunks_exact(2)
.filter_map(|kv| {
let mut kc = kv[0].chars();
let mut vc = kv[1].chars();
match (kc.next(), kc.next(), vc.next(), vc.next()) {
(Some(k), None, Some(v), None) => Some((k, v)),
_ => None,
}
})
.collect();
if !pairs.is_empty() {
cfg.pairs = pairs;
}
}
if let Some(flat) = gethparam("AUTOPAIR_LBOUNDS") {
for kv in flat.chunks_exact(2) {
cfg.lbounds.insert(kv[0].clone(), kv[1].clone());
}
}
if let Some(flat) = gethparam("AUTOPAIR_RBOUNDS") {
for kv in flat.chunks_exact(2) {
cfg.rbounds.insert(kv[0].clone(), kv[1].clone());
}
}
cfg.between_whitespace = getsparam("AUTOPAIR_BETWEEN_WHITESPACE")
.map(|v| !v.is_empty())
.unwrap_or(false);
cfg
}
fn lbound(&self, group: &str) -> Option<String> {
self.lbounds
.get(group)
.cloned()
.or_else(|| default_lbound(group).map(str::to_owned))
}
fn rbound(&self, group: &str) -> Option<String> {
self.rbounds
.get(group)
.cloned()
.or_else(|| default_rbound(group).map(str::to_owned))
}
}
pub fn get_pair_by_open(cfg: &AutopairConfig, open: char) -> Option<char> {
cfg.pairs.iter().find(|(o, _)| *o == open).map(|(_, c)| *c)
}
pub fn get_pair_by_close(cfg: &AutopairConfig, close: char) -> Option<char> {
cfg.pairs.iter().find(|(_, c)| *c == close).map(|(o, _)| *o)
}
static REGEX_CACHE: Mutex<Option<HashMap<String, Option<regex::Regex>>>> = Mutex::new(None);
fn cached_regex_matches(pattern: &str, hay: &str) -> bool {
if pattern.is_empty() {
return false;
}
let mut guard = REGEX_CACHE.lock().unwrap();
let map = guard.get_or_insert_with(HashMap::new);
if map.len() > 256 {
map.clear();
}
let entry = map.entry(pattern.to_owned()).or_insert_with(|| {
regex::Regex::new(pattern)
.ok()
.or_else(|| regex::Regex::new(&zsh_ere_to_rust(pattern)).ok())
});
entry.as_ref().map(|re| re.is_match(hay)).unwrap_or(false)
}
fn boundary_p(l_pat: Option<&str>, r_pat: Option<&str>, lbuf: &str, rbuf: &str) -> bool {
let l_hit = l_pat
.filter(|p| !p.is_empty())
.map(|p| cached_regex_matches(&format!("(?:{p})$"), lbuf))
.unwrap_or(false);
let r_hit = r_pat
.filter(|p| !p.is_empty())
.map(|p| cached_regex_matches(&format!("^(?:{p})"), rbuf))
.unwrap_or(false);
l_hit || r_hit
}
fn next_to_boundary_p(cfg: &AutopairConfig, key: char, lbuf: &str, rbuf: &str) -> bool {
let mut groups: Vec<String> = vec!["all".to_owned()];
match key {
'\'' | '"' | '`' => groups.push("quotes".to_owned()),
'{' | '[' | '(' | '<' => groups.push("braces".to_owned()),
' ' => groups.push("spaces".to_owned()),
_ => (),
}
groups.push(key.to_string()); for group in &groups {
if boundary_p(
cfg.lbound(group).as_deref(),
cfg.rbound(group).as_deref(),
lbuf,
rbuf,
) {
return true;
}
}
false
}
fn unescaped_count(s: &str, ch: char) -> usize {
let stripped: String = {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' && chars.peek() == Some(&ch) {
chars.next();
continue;
}
out.push(c);
}
out
};
stripped.chars().filter(|&c| c == ch).count()
}
fn balanced_p(open: char, close: char, lbuf: &str, rbuf: &str) -> bool {
let llen = unescaped_count(lbuf, open);
let rlen = unescaped_count(rbuf, close);
if rlen == 0 && llen == 0 {
return true; }
if open == close {
if open == ' ' {
let lchars: Vec<char> = lbuf.chars().collect();
let mut i = lchars.len();
while i > 0 && matches!(lchars[i - 1], ' ' | '\t') {
i -= 1;
}
let run: String = lchars[i..].iter().collect();
let preceded_ok = !run.is_empty()
&& (i == 0 || !matches!(lchars[i - 1], '\'' | '"'));
return preceded_ok && !run.is_empty() && rbuf.starts_with(&run);
} else if llen == rlen || (llen + rlen) % 2 == 0 {
return true; }
} else {
let l2len = unescaped_count(lbuf, close);
let r2len = unescaped_count(rbuf, open);
let ltotal = (llen as i64 - l2len as i64).max(0);
let rtotal = rlen as i64 - r2len as i64;
return ltotal >= rtotal;
}
false }
fn can_pair_p(cfg: &AutopairConfig, key: char, lbuf: &str, rbuf: &str) -> bool {
let Some(rchar) = get_pair_by_open(cfg, key) else {
return false; };
if rchar != ' ' {
if cfg.between_whitespace
&& lbuf
.chars()
.next_back()
.map(|c| matches!(c, ' ' | '\t'))
.unwrap_or(true)
&& rbuf
.chars()
.next()
.map(|c| matches!(c, ' ' | '\t'))
.unwrap_or(true)
{
return true;
}
if !balanced_p(key, rchar, lbuf, rbuf) {
return false;
}
} else if rbuf.chars().all(|c| matches!(c, ' ' | '\t')) {
return false;
}
if next_to_boundary_p(cfg, key, lbuf, rbuf) {
return false;
}
true
}
fn can_skip_p(open: Option<char>, close: Option<char>, lbuf: &str, rbuf: &str) -> bool {
if lbuf.is_empty() {
return false; }
let Some(close) = close else {
return false;
};
if open == Some(close) {
if close == ' ' {
return false; }
if !balanced_p(close, close, lbuf, rbuf) {
return false; }
}
rbuf.starts_with(close) && !lbuf.ends_with('\\')
}
fn can_delete_p(cfg: &AutopairConfig, lbuf: &str, rbuf: &str) -> bool {
let Some(lchar) = lbuf.chars().next_back() else {
return false;
};
let Some(rchar) = get_pair_by_open(cfg, lchar) else {
return false; };
if !rbuf.starts_with(rchar) {
return false; }
if lchar == rchar {
if lchar == ' ' {
if cached_regex_matches(r"[^{(\[] +$", lbuf)
|| cached_regex_matches(r"^ +[^\]})]", rbuf)
{
return false;
}
} else if !balanced_p(lchar, rchar, lbuf, rbuf) {
return false; }
}
true
}
#[derive(Debug, PartialEq, Eq)]
pub enum Action {
InsertPair(char, char),
SkipOver,
DeleteRightThenPassthrough,
Passthrough,
}
pub fn decide(
cfg: &AutopairConfig,
widget: &str,
key: Option<char>,
lbuf: &str,
rbuf: &str,
) -> Action {
match widget {
"self-insert" => {
let Some(key) = key else {
return Action::Passthrough;
};
let as_open = get_pair_by_open(cfg, key);
if let Some(rchar) = as_open {
if matches!(key, '\'' | '"' | '`' | ' ')
&& can_skip_p(Some(key), Some(rchar), lbuf, rbuf)
{
return Action::SkipOver; }
if can_pair_p(cfg, key, lbuf, rbuf) {
return Action::InsertPair(key, rchar); }
return Action::Passthrough; }
if let Some(open) = get_pair_by_close(cfg, key) {
if can_skip_p(Some(open), Some(key), lbuf, rbuf) {
return Action::SkipOver; }
}
Action::Passthrough }
"backward-delete-char"
| "vi-backward-delete-char"
| "backward-delete-word"
| "backward-kill-word" => {
if can_delete_p(cfg, lbuf, rbuf) {
Action::DeleteRightThenPassthrough
} else {
Action::Passthrough
}
}
_ => Action::Passthrough,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> AutopairConfig {
AutopairConfig::default()
}
#[test]
fn paren_pairs_on_empty_line() {
assert_eq!(
decide(&cfg(), "self-insert", Some('('), "", ""),
Action::InsertPair('(', ')')
);
assert_eq!(
decide(&cfg(), "self-insert", Some('['), "echo ", ""),
Action::InsertPair('[', ']')
);
assert_eq!(
decide(&cfg(), "self-insert", Some('{'), "x=", ""),
Action::InsertPair('{', '}')
);
}
#[test]
fn no_pair_before_word_char() {
assert_eq!(
decide(&cfg(), "self-insert", Some('('), "", "foo"),
Action::Passthrough
);
}
#[test]
fn no_quote_pair_after_word_char() {
assert_eq!(
decide(&cfg(), "self-insert", Some('\''), "don", ""),
Action::Passthrough
);
}
#[test]
fn quote_pairs_on_fresh_word() {
assert_eq!(
decide(&cfg(), "self-insert", Some('"'), "echo ", ""),
Action::InsertPair('"', '"')
);
}
#[test]
fn closer_skips_over() {
assert_eq!(
decide(&cfg(), "self-insert", Some(')'), "echo (", ")"),
Action::SkipOver
);
assert_eq!(
decide(&cfg(), "self-insert", Some(')'), "echo (", " x"),
Action::Passthrough
);
}
#[test]
fn quote_skips_over_balanced() {
assert_eq!(
decide(&cfg(), "self-insert", Some('\''), "echo 'hi", "'"),
Action::SkipOver
);
}
#[test]
fn escaped_closer_not_skipped() {
assert_eq!(
decide(&cfg(), "self-insert", Some(')'), "echo (\\", ")x"),
Action::Passthrough
);
}
#[test]
fn backspace_deletes_pair() {
assert_eq!(
decide(&cfg(), "backward-delete-char", None, "echo (", ")"),
Action::DeleteRightThenPassthrough
);
assert_eq!(
decide(&cfg(), "backward-delete-char", None, "echo (", "x)"),
Action::Passthrough
);
}
#[test]
fn unbalanced_quote_does_not_pair() {
assert_eq!(
decide(&cfg(), "self-insert", Some('\''), "echo 'abc ", ""),
Action::Passthrough
);
}
#[test]
fn escaped_quote_ignored_in_balance() {
assert_eq!(
decide(&cfg(), "self-insert", Some('\''), "echo \\' ", ""),
Action::InsertPair('\'', '\'')
);
}
#[test]
fn space_needs_delimiters() {
assert_eq!(
decide(&cfg(), "self-insert", Some(' '), "echo", " "),
Action::Passthrough
);
assert_eq!(
decide(&cfg(), "self-insert", Some(' '), "x={", "}"),
Action::InsertPair(' ', ' ')
);
}
#[test]
fn space_collapse_only_in_delimiters() {
assert_eq!(
decide(&cfg(), "backward-delete-char", None, "x={ ", " }"),
Action::DeleteRightThenPassthrough
);
assert_eq!(
decide(&cfg(), "backward-delete-char", None, "echo a ", " b"),
Action::Passthrough
);
}
#[test]
fn between_whitespace_forces_pair() {
let mut c = cfg();
c.between_whitespace = true;
assert_eq!(
decide(&c, "self-insert", Some('('), "word ", " word"),
Action::InsertPair('(', ')')
);
}
}