#![allow(non_snake_case)]
use crate::autosuggest::{self, AutosuggestionPortion};
use crate::history_search::{with_history_search, SearchDirection, SearchMode};
use crate::ported::params::getsparam;
use crate::ported::zle::zle_main::{ZLECS, ZLELINE, ZLELL};
use crate::ported::zle::zle_refresh::TextAttr;
use crate::syntax_highlight::{highlight_shell, HighlightColorResolver, HighlightSpec};
use crate::zle_file_tester::OperationContext;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::Mutex;
#[derive(Default)]
struct FxState {
line_attrs: Vec<Option<TextAttr>>,
hl_line: String,
ghost_attr: Option<TextAttr>,
search_placed: Option<String>,
}
static FX: Mutex<Option<FxState>> = Mutex::new(None);
fn with_fx<R>(f: impl FnOnce(&mut FxState) -> R) -> R {
let mut guard = FX.lock().unwrap();
f(guard.get_or_insert_with(Default::default))
}
pub fn enabled() -> bool {
match getsparam("ZSHRS_NATIVE_ZLE_FX") {
Some(v) => v != "0",
None => std::env::var("ZSHRS_NATIVE_ZLE_FX").map(|v| v != "0").unwrap_or(true),
}
}
fn autosuggest_active() -> bool {
enabled() && crate::config::current().zle.autosuggest
}
fn highlight_active() -> bool {
enabled() && crate::config::current().zle.syntax_highlight
}
fn search_active() -> bool {
enabled() && crate::config::current().zle.history_search
}
fn autopair_engine_active() -> bool {
enabled()
&& crate::config::current().zle.autopair
&& crate::ported::zle::zle_hist::ISEARCH_ACTIVE.load(SeqCst) == 0
&& !crate::ported::zle::zle_thingy::rthingy_nocreate("autopair-insert")
}
fn current_line() -> String {
ZLELINE.lock().unwrap().iter().collect()
}
fn current_cursor() -> usize {
ZLECS.load(SeqCst)
}
fn set_buffer(text: &str, cursor: usize) {
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
*ZLELINE.lock().unwrap() = chars;
ZLELL.store(n, SeqCst);
ZLECS.store(cursor.min(n), SeqCst);
if crate::ported::zle::zle_main::VIINSBEGIN.load(SeqCst) > ZLECS.load(SeqCst) {
crate::ported::zle::zle_main::VIINSBEGIN.store(0, SeqCst);
}
}
fn tail_is_autopair_closers() -> bool {
let line = current_line();
let cursor = current_cursor().min(line.chars().count());
let tail: Vec<char> = line.chars().skip(cursor).collect();
if tail.is_empty() {
return false; }
let cfg = crate::autopair::AutopairConfig::from_params();
tail.iter()
.all(|&c| cfg.pairs.iter().any(|(_, close)| *close == c))
}
fn cursor_at_end() -> bool {
let cs = ZLECS.load(SeqCst);
let ll = ZLELL.load(SeqCst);
if crate::ported::zle::zle_main::in_vi_cmd_mode() {
cs + 1 >= ll
} else {
cs >= ll
}
}
const ACCEPT_FULL_WIDGETS: &[&str] = &[
"forward-char",
"vi-forward-char",
"end-of-line",
"vi-end-of-line",
"vi-add-eol",
];
const ACCEPT_PARTIAL_WIDGETS: &[&str] = &[
"forward-word",
"emacs-forward-word",
"vi-forward-word",
"vi-forward-word-end",
"vi-forward-blank-word",
"vi-forward-blank-word-end",
];
const DELETE_WIDGETS: &[&str] = &[
"backward-delete-char",
"vi-backward-delete-char",
"delete-char",
"delete-char-or-list",
"backward-kill-word",
"backward-kill-line",
"kill-word",
"kill-line",
"kill-whole-line",
"kill-buffer",
"vi-delete",
"vi-delete-char",
"vi-kill-line",
"vi-kill-eol",
"vi-change",
"vi-change-eol",
"vi-change-whole-line",
];
const INSERT_WIDGETS: &[&str] = &[
"self-insert",
"self-insert-unmeta",
"magic-space",
"bracketed-paste",
"quoted-insert",
"vi-put-after",
"vi-put-before",
"yank",
];
fn up_search_mode(widget: &str) -> Option<SearchMode> {
match widget {
"up-line-or-search" | "history-beginning-search-backward" => Some(SearchMode::Prefix),
"history-substring-search-up" => Some(SearchMode::Line),
"up-line-or-history" | "up-history" => Some(SearchMode::Line),
_ => None,
}
}
fn down_search_widget(widget: &str) -> bool {
matches!(
widget,
"down-line-or-search"
| "history-beginning-search-forward"
| "history-substring-search-down"
| "down-line-or-history"
| "down-history"
)
}
pub fn on_pre_widget(widget: &str) -> bool {
if !enabled() {
return false;
}
if matches!(
widget,
"self-insert"
| "backward-delete-char"
| "vi-backward-delete-char"
| "backward-delete-word"
| "backward-kill-word"
) && autopair_engine_active()
{
let key = {
let lc = crate::ported::zle::compcore::LASTCHAR.load(SeqCst);
(0..=0x7f).contains(&lc).then(|| lc as u8 as char)
};
let line = current_line();
let cursor = current_cursor().min(line.chars().count());
let lbuf: String = line.chars().take(cursor).collect();
let rbuf: String = line.chars().skip(cursor).collect();
let cfg = crate::autopair::AutopairConfig::from_params();
match crate::autopair::decide(&cfg, widget, key, &lbuf, &rbuf) {
crate::autopair::Action::InsertPair(open, close) => {
let new_line = format!("{lbuf}{open}{close}{rbuf}");
set_buffer(&new_line, cursor + 1);
return true;
}
crate::autopair::Action::SkipOver => {
ZLECS.store((cursor + 1).min(ZLELL.load(SeqCst)), SeqCst);
return true;
}
crate::autopair::Action::DeleteRightThenPassthrough => {
let vi_blocked = widget == "vi-backward-delete-char"
&& cursor <= crate::ported::zle::zle_main::VIINSBEGIN.load(SeqCst);
if !vi_blocked {
let rest: String = rbuf.chars().skip(1).collect();
let new_line = format!("{lbuf}{rest}");
set_buffer(&new_line, cursor);
}
}
crate::autopair::Action::Passthrough => (),
}
}
if autosuggest_active() && (cursor_at_end() || tail_is_autopair_closers()) {
let portion = if ACCEPT_FULL_WIDGETS.contains(&widget) {
Some(AutosuggestionPortion::Count(usize::MAX))
} else if ACCEPT_PARTIAL_WIDGETS.contains(&widget) {
Some(AutosuggestionPortion::Word)
} else {
None
};
if let Some(portion) = portion {
let line = current_line();
let accepted = autosuggest::with_state(|st| {
if !autosuggest::is_at_line_with_autosuggestion(st, &line, current_cursor()) {
return None;
}
autosuggest::accept_autosuggestion(st, portion)
});
if let Some((range, replacement)) = accepted {
let chars: Vec<char> = line.chars().collect();
let mut new_line: String = chars[..range.start].iter().collect();
new_line.push_str(&replacement);
new_line.extend(chars[range.end.min(chars.len())..].iter());
let new_cursor = range.start + replacement.chars().count();
set_buffer(&new_line, new_cursor);
crate::ported::zle::zle_params::set_postdisplay(Some(""));
with_fx(|fx| fx.ghost_attr = None);
return true;
}
}
}
if search_active() {
if let Some(mode) = up_search_mode(widget) {
let line = current_line();
if line.is_empty() && matches!(widget, "up-line-or-history" | "up-history") {
return false;
}
let placed = with_history_search(|hs| {
if !hs.active() {
hs.reset_to_mode(line.clone(), mode, 0);
}
if hs.move_in_direction(SearchDirection::Backward) {
Some(hs.current_result().to_owned())
} else {
None }
});
autosuggest::with_state(|st| st.history_search_active = true);
if let Some(text) = placed {
let end = text.chars().count();
set_buffer(&text, end);
with_fx(|fx| fx.search_placed = Some(text));
}
return true;
}
if down_search_widget(widget) {
let active = with_history_search(|hs| hs.active());
if !active {
return false; }
let placed = with_history_search(|hs| {
if hs.move_in_direction(SearchDirection::Forward) || hs.is_at_present() {
Some(hs.current_result().to_owned())
} else {
None
}
});
if let Some(text) = placed {
let end = text.chars().count();
set_buffer(&text, end);
let at_present = with_history_search(|hs| hs.is_at_present());
with_fx(|fx| fx.search_placed = Some(text));
if at_present {
with_history_search(|hs| hs.reset());
autosuggest::with_state(|st| st.history_search_active = false);
with_fx(|fx| fx.search_placed = None);
}
}
return true;
}
}
false
}
pub fn on_post_widget(widget: &str) {
if !enabled() {
return;
}
let line = current_line();
let cursor = current_cursor();
if search_active() {
let edited = with_fx(|fx| {
fx.search_placed
.as_ref()
.map(|placed| placed != &line)
.unwrap_or(false)
});
if edited {
with_history_search(|hs| hs.reset());
autosuggest::with_state(|st| st.history_search_active = false);
with_fx(|fx| fx.search_placed = None);
}
}
if crate::ported::zle::zle_misc::DONE.load(SeqCst) != 0
|| matches!(
widget,
"accept-line"
| "vi-accept-line"
| "accept-line-and-down-history"
| "accept-and-hold"
| "accept-and-infer-next-history"
| "send-break"
)
{
with_fx(|fx| {
fx.ghost_attr = None;
fx.search_placed = None;
});
autosuggest::with_state(autosuggest::on_line_finish);
with_history_search(|hs| hs.reset());
crate::ported::zle::zle_params::set_postdisplay(Some(""));
return;
}
if autosuggest_active() {
let t0 = std::time::Instant::now();
autosuggest::with_state(|st| {
if DELETE_WIDGETS.contains(&widget) {
autosuggest::on_delete(st);
} else if INSERT_WIDGETS.contains(&widget) {
autosuggest::on_insert(st, &line);
}
});
let ctx = OperationContext::with_budget(std::time::Duration::from_millis(budget_ms(
"ZSHRS_ZLE_AUTOSUGGEST_BUDGET_MS",
8,
)));
autosuggest::update_autosuggestion(
&line,
cursor,
&autosuggest::history_commands_newest_first,
&ctx,
);
let suffix = autosuggest::with_state(|st| st.autosuggestion.suffix().to_owned());
crate::ported::zle::zle_params::set_postdisplay(Some(&suffix));
let ghost = if suffix.is_empty() {
None
} else {
Some(zattr_to_text_attr(
HighlightColorResolver::resolve_spec_uncached(&HighlightSpec::with_fg(
crate::syntax_highlight::HighlightRole::autosuggestion,
)),
))
};
with_fx(|fx| fx.ghost_attr = ghost);
let dt = t0.elapsed();
if dt.as_millis() >= 20 {
tracing::debug!(target: "zle_fx", ms = dt.as_millis() as u64, "slow autosuggest pass");
}
}
if highlight_active() {
let t0 = std::time::Instant::now();
with_fx(|fx| {
if fx.hl_line != line {
if line.is_empty() {
fx.line_attrs.clear();
} else {
let meta = crate::ported::utils::metafy(&line);
let mut colors: Vec<HighlightSpec> = Vec::new();
let ctx = OperationContext::with_budget(std::time::Duration::from_millis(
budget_ms("ZSHRS_ZLE_HIGHLIGHT_BUDGET_MS", 8),
));
highlight_shell(&meta, &mut colors, &ctx, true, Some(cursor));
let mut resolver = HighlightColorResolver::new();
let raw_len = line.chars().count();
let mut attrs: Vec<Option<TextAttr>> = vec![None; raw_len];
if meta == line {
for (i, spec) in colors.iter().enumerate().take(raw_len) {
attrs[i] = spec_to_attr(&mut resolver, spec);
}
} else {
let mut mi = 0usize;
for (ri, ch) in line.chars().enumerate() {
let w = crate::ported::utils::metafy(&ch.to_string())
.chars()
.count();
if let Some(spec) = colors.get(mi) {
attrs[ri] = spec_to_attr(&mut resolver, spec);
}
mi += w;
}
}
fx.line_attrs = attrs;
}
fx.hl_line = line.clone();
}
});
if search_active() {
let found = with_history_search(|hs| hs.search_range_if_active());
if let Some((start, len)) = found {
let spec = getsparam("HISTORY_SUBSTRING_SEARCH_HIGHLIGHT_FOUND")
.unwrap_or_else(|| "bg=magenta,fg=white,bold".to_owned());
let (mask_on, _off) = crate::ported::prompt::match_highlight(&spec);
let attr = zattr_to_text_attr(mask_on);
with_fx(|fx| {
for i in start..(start + len).min(fx.line_attrs.len()) {
fx.line_attrs[i] = Some(attr);
}
});
}
}
let dt = t0.elapsed();
if dt.as_millis() >= 20 {
tracing::debug!(target: "zle_fx", ms = dt.as_millis() as u64, "slow highlight pass");
}
}
}
fn budget_ms(name: &str, default: u64) -> u64 {
getsparam(name)
.or_else(|| std::env::var(name).ok())
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(default)
}
pub fn native_render_attrs(
attrs: &mut [Option<TextAttr>],
pre_len: usize,
line_len: usize,
) {
if !enabled() {
return;
}
with_fx(|fx| {
for (i, a) in fx.line_attrs.iter().enumerate().take(line_len) {
if let Some(a) = a {
if let Some(slot) = attrs.get_mut(pre_len + i) {
*slot = Some(*a);
}
}
}
if let Some(ghost) = fx.ghost_attr {
for slot in attrs.iter_mut().skip(pre_len + line_len) {
*slot = Some(ghost);
}
}
});
}
fn spec_to_attr(
resolver: &mut HighlightColorResolver,
spec: &HighlightSpec,
) -> Option<TextAttr> {
if *spec == HighlightSpec::default() {
return None;
}
let z = resolver.resolve_spec(spec);
if z == 0 {
return None;
}
Some(zattr_to_text_attr(z))
}
fn zattr_to_text_attr(a: crate::ported::zsh_h::zattr) -> TextAttr {
use crate::ported::zsh_h::{
zattr, TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR, TXTSTANDOUT, TXTUNDERLINE,
TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_FG_COL_SHIFT,
};
TextAttr {
bold: a & TXTBOLDFACE != 0,
underline: a & TXTUNDERLINE != 0,
standout: a & TXTSTANDOUT != 0,
blink: false,
fg_color: (a & TXTFGCOLOUR != 0)
.then(|| ((a >> TXT_ATTR_FG_COL_SHIFT) & 0xff as zattr) as u8),
bg_color: (a & TXTBGCOLOUR != 0)
.then(|| ((a >> TXT_ATTR_BG_COL_SHIFT) & 0xff as zattr) as u8),
}
}
pub fn on_line_finish() {
with_fx(|fx| {
fx.line_attrs.clear();
fx.hl_line.clear();
fx.ghost_attr = None;
fx.search_placed = None;
});
autosuggest::with_state(autosuggest::on_line_finish);
with_history_search(|hs| hs.reset());
crate::ported::zle::zle_params::set_postdisplay(Some(""));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zattr_round_trip() {
use crate::ported::zsh_h::{
TXTBOLDFACE, TXTFGCOLOUR, TXTUNDERLINE, TXT_ATTR_FG_COL_SHIFT,
};
let a = TXTBOLDFACE | TXTUNDERLINE | TXTFGCOLOUR | ((2u64) << TXT_ATTR_FG_COL_SHIFT);
let t = zattr_to_text_attr(a);
assert!(t.bold && t.underline && !t.standout);
assert_eq!(t.fg_color, Some(2));
assert_eq!(t.bg_color, None);
}
#[test]
fn accept_widget_tables_are_disjoint() {
for w in ACCEPT_FULL_WIDGETS {
assert!(!ACCEPT_PARTIAL_WIDGETS.contains(w));
assert!(!DELETE_WIDGETS.contains(w));
}
for w in DELETE_WIDGETS {
assert!(!INSERT_WIDGETS.contains(w));
}
}
}