#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use crate::ported::exec::findcmd;
use crate::ported::hashtable::{aliastab_lock, cmdnamtab_lock, reswdtab_lock, sufaliastab_lock};
use crate::ported::lex::{
ctxtlex, incmdpos, inredir, tok, tokstr, untokenize, LEX_LEXFLAGS, LEX_WORDBEG,
};
use crate::ported::params::{gethkparam, gethparam, getsparam};
use crate::ported::prompt::match_highlight;
use crate::ported::utils::getshfunc;
use crate::ported::zsh_h::{
isset, lextok, zattr, AMPER, AMPERBANG, AUTOCD, BAR_TOK, CASE, CLOBBER, DAMPER, DBAR, DINANG,
DINANGDASH, DINBRACK, DINPAR, DOUTANG, DOUTANGAMP, DOUTANGAMPBANG, DOUTANGBANG, DOUTBRACK,
BANG_TOK, BARAMP, DOUTPAR, DSEMI, ENDINPUT, ENVARRAY, ENVSTRING, INANGAMP, INANG_TOK, INBRACE_TOK, INOUTANG,
INOUTPAR, INPAR_TOK,
INTERACTIVECOMMENTS, IS_REDIROP, LEXERR, LEXFLAGS_ACTIVE, LEXFLAGS_ZLE, NEWLIN, OUTANGAMP,
OUTANGAMPBANG, OUTANGBANG, OUTANG_TOK, OUTBRACE_TOK, OUTPAR_TOK, SEMI, SEMIAMP, SEMIBAR, SEPER,
STRING_LEX, TRINANG, TYPESET,
};
use crate::zle_file_tester::{
expand_one_no_cmdsubst, FileTester, IsErr, IsFile, OperationContext, RedirectionMode,
};
use std::collections::{hash_map::Entry, HashMap};
use std::sync::Mutex;
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub struct HighlightSpec {
pub foreground: HighlightRole,
pub background: HighlightRole,
pub valid_path: bool,
pub force_underline: bool,
pub secondary: bool,
}
impl HighlightSpec {
pub fn new() -> Self {
Self::default()
}
pub fn with_fg_bg(fg: HighlightRole, bg: HighlightRole) -> Self {
Self {
foreground: fg,
background: bg,
..Default::default()
}
}
pub fn with_fg(fg: HighlightRole) -> Self {
Self::with_fg_bg(fg, HighlightRole::normal)
}
pub fn with_bg(bg: HighlightRole) -> Self {
Self::with_fg_bg(HighlightRole::normal, bg)
}
pub fn with_both(role: HighlightRole) -> Self {
Self::with_fg_bg(role, role)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[repr(u8)]
pub enum HighlightRole {
#[default]
normal, error, command, keyword,
statement_terminator, param, option, comment, search_match, operat, escape, quote, redirection, autosuggestion, selection,
pager_progress,
pager_background,
pager_prefix,
pager_completion,
pager_description,
pager_secondary_background,
pager_secondary_prefix,
pager_secondary_completion,
pager_secondary_description,
pager_selected_background,
pager_selected_prefix,
pager_selected_completion,
pager_selected_description,
precommand, alias_, suffix_alias, global_alias, builtin_, function_, hashed_command, path, path_to_dir, globbing, history_expansion, double_hyphen_option, dquoted, dollar_quoted, dollar_in_dquote, variable, mathvar, mathnum, matherr, assign, assign_array_bracket, here_string_tri, here_string_text, single_sq_bracket, double_sq_bracket, double_paren, bracket_level_1, bracket_level_2, bracket_level_3, case_input, case_parentheses, case_condition, globbing_ext, here_string_var, for_loop_variable, for_loop_operator, for_loop_number, for_loop_separator, correct_subtle, incorrect_subtle, }
pub type ColorArray = Vec<HighlightSpec>;
fn get_highlight_style_key(role: HighlightRole) -> &'static str {
match role {
HighlightRole::normal => "default",
HighlightRole::error => "unknown-token",
HighlightRole::command => "command",
HighlightRole::keyword => "reserved-word",
HighlightRole::statement_terminator => "commandseparator",
HighlightRole::param => "default",
HighlightRole::option => "single-hyphen-option",
HighlightRole::comment => "comment",
HighlightRole::search_match => "history-search-match",
HighlightRole::operat => "globbing",
HighlightRole::escape => "back-dollar-quoted-argument",
HighlightRole::quote => "single-quoted-argument",
HighlightRole::redirection => "redirection",
HighlightRole::autosuggestion => "autosuggestion",
HighlightRole::selection => "selection",
HighlightRole::precommand => "precommand",
HighlightRole::alias_ => "alias",
HighlightRole::suffix_alias => "suffix-alias",
HighlightRole::global_alias => "global-alias",
HighlightRole::builtin_ => "builtin",
HighlightRole::function_ => "function",
HighlightRole::hashed_command => "hashed-command",
HighlightRole::path => "path",
HighlightRole::path_to_dir => "path-to-dir",
HighlightRole::globbing => "globbing",
HighlightRole::history_expansion => "history-expansion",
HighlightRole::double_hyphen_option => "double-hyphen-option",
HighlightRole::dquoted => "double-quoted-argument",
HighlightRole::dollar_quoted => "dollar-quoted-argument",
HighlightRole::dollar_in_dquote => "back-or-dollar-double-quoted-argument",
HighlightRole::variable => "variable",
HighlightRole::mathvar => "mathvar",
HighlightRole::mathnum => "mathnum",
HighlightRole::matherr => "matherr",
HighlightRole::assign => "assign",
HighlightRole::assign_array_bracket => "assign-array-bracket",
HighlightRole::here_string_tri => "here-string-tri",
HighlightRole::here_string_text => "here-string-text",
HighlightRole::single_sq_bracket => "single-sq-bracket",
HighlightRole::double_sq_bracket => "double-sq-bracket",
HighlightRole::double_paren => "double-paren",
HighlightRole::bracket_level_1 => "bracket-level-1",
HighlightRole::bracket_level_2 => "bracket-level-2",
HighlightRole::bracket_level_3 => "bracket-level-3",
HighlightRole::case_input => "case-input",
HighlightRole::case_parentheses => "case-parentheses",
HighlightRole::case_condition => "case-condition",
HighlightRole::globbing_ext => "globbing-ext",
HighlightRole::here_string_var => "here-string-var",
HighlightRole::for_loop_variable => "for-loop-variable",
HighlightRole::for_loop_operator => "for-loop-operator",
HighlightRole::for_loop_number => "for-loop-number",
HighlightRole::for_loop_separator => "for-loop-separator",
HighlightRole::correct_subtle => "correct-subtle",
HighlightRole::incorrect_subtle => "incorrect-subtle",
_ => "default",
}
}
fn get_default_style(role: HighlightRole) -> &'static str {
match role {
HighlightRole::error => "fg=red,bold",
HighlightRole::command
| HighlightRole::precommand
| HighlightRole::alias_
| HighlightRole::suffix_alias
| HighlightRole::builtin_
| HighlightRole::function_
| HighlightRole::hashed_command => "fg=green",
HighlightRole::global_alias => "bg=blue", HighlightRole::keyword => "fg=yellow", HighlightRole::comment => "fg=black,bold", HighlightRole::operat | HighlightRole::globbing => "fg=blue,bold", HighlightRole::escape => "fg=cyan", HighlightRole::quote | HighlightRole::dquoted | HighlightRole::dollar_quoted => {
"fg=yellow" }
HighlightRole::redirection => "none", HighlightRole::autosuggestion => "fg=8",
HighlightRole::selection | HighlightRole::search_match => "standout",
HighlightRole::option | HighlightRole::double_hyphen_option => "fg=cyan", HighlightRole::path => "fg=magenta", HighlightRole::path_to_dir => "fg=magenta,underline", HighlightRole::history_expansion => "fg=blue,bold", HighlightRole::dollar_in_dquote => "fg=cyan", HighlightRole::variable => "fg=113", HighlightRole::mathvar => "fg=blue,bold", HighlightRole::mathnum => "fg=magenta", HighlightRole::matherr => "fg=red", HighlightRole::assign => "none", HighlightRole::assign_array_bracket => "fg=green", HighlightRole::here_string_tri => "fg=yellow", HighlightRole::here_string_text => "bg=18", HighlightRole::single_sq_bracket | HighlightRole::double_sq_bracket => "fg=green", HighlightRole::double_paren => "fg=yellow", HighlightRole::bracket_level_1 => "fg=green,bold", HighlightRole::bracket_level_2 => "fg=yellow,bold", HighlightRole::bracket_level_3 => "fg=cyan,bold", HighlightRole::case_input => "fg=green", HighlightRole::case_parentheses => "fg=yellow", HighlightRole::case_condition => "bg=blue", HighlightRole::globbing_ext => "fg=13", HighlightRole::here_string_var => "fg=cyan,bg=18", HighlightRole::for_loop_variable => "none", HighlightRole::for_loop_operator => "fg=yellow", HighlightRole::for_loop_number => "fg=magenta", HighlightRole::for_loop_separator => "fg=yellow,bold", HighlightRole::correct_subtle => "fg=12", HighlightRole::incorrect_subtle => "fg=red", _ => "none",
}
}
fn get_secondary_default_style(role: HighlightRole) -> &'static str {
match role {
HighlightRole::error => "fg=red,bold",
HighlightRole::command
| HighlightRole::precommand
| HighlightRole::alias_
| HighlightRole::suffix_alias
| HighlightRole::builtin_
| HighlightRole::function_
| HighlightRole::hashed_command
| HighlightRole::case_input => "fg=180",
HighlightRole::keyword => "fg=150",
HighlightRole::global_alias | HighlightRole::case_condition => "bg=19",
HighlightRole::path => "fg=166",
HighlightRole::path_to_dir => "fg=166,underline",
HighlightRole::operat | HighlightRole::globbing => "fg=112",
HighlightRole::history_expansion => "fg=blue,bold",
HighlightRole::option | HighlightRole::double_hyphen_option => "fg=110",
HighlightRole::quote | HighlightRole::dquoted | HighlightRole::dollar_quoted => "fg=150",
HighlightRole::escape | HighlightRole::dollar_in_dquote => "fg=110",
HighlightRole::comment => "fg=black,bold",
HighlightRole::variable => "none",
HighlightRole::mathvar => "fg=blue,bold",
HighlightRole::mathnum => "fg=166",
HighlightRole::matherr => "fg=red",
HighlightRole::assign_array_bracket => "fg=180",
HighlightRole::here_string_tri => "fg=yellow",
HighlightRole::here_string_text => "bg=19",
HighlightRole::single_sq_bracket | HighlightRole::double_sq_bracket => "fg=180",
HighlightRole::double_paren => "fg=150",
HighlightRole::case_parentheses => "fg=116",
HighlightRole::globbing_ext => "fg=118",
HighlightRole::here_string_var => "fg=110,bg=19",
HighlightRole::for_loop_variable => "none",
HighlightRole::for_loop_operator => "fg=150",
HighlightRole::for_loop_number => "fg=150",
HighlightRole::for_loop_separator => "fg=109",
HighlightRole::correct_subtle => "bg=55",
HighlightRole::incorrect_subtle => "bg=52",
HighlightRole::bracket_level_1 => "fg=130",
HighlightRole::bracket_level_2 => "fg=70",
HighlightRole::bracket_level_3 => "fg=69",
HighlightRole::autosuggestion => "fg=8",
HighlightRole::selection | HighlightRole::search_match => "standout",
_ => "none",
}
}
fn get_fallback(role: HighlightRole) -> HighlightRole {
match role {
HighlightRole::normal
| HighlightRole::error
| HighlightRole::command
| HighlightRole::statement_terminator
| HighlightRole::param
| HighlightRole::search_match
| HighlightRole::comment
| HighlightRole::operat
| HighlightRole::escape
| HighlightRole::quote
| HighlightRole::redirection
| HighlightRole::autosuggestion
| HighlightRole::selection
| HighlightRole::pager_progress
| HighlightRole::pager_background
| HighlightRole::pager_prefix
| HighlightRole::pager_completion
| HighlightRole::pager_description => HighlightRole::normal,
HighlightRole::keyword
| HighlightRole::option
| HighlightRole::precommand
| HighlightRole::alias_
| HighlightRole::suffix_alias
| HighlightRole::global_alias
| HighlightRole::builtin_
| HighlightRole::function_
| HighlightRole::hashed_command
| HighlightRole::path
| HighlightRole::path_to_dir
| HighlightRole::globbing
| HighlightRole::history_expansion
| HighlightRole::double_hyphen_option
| HighlightRole::dquoted
| HighlightRole::dollar_quoted
| HighlightRole::dollar_in_dquote
| HighlightRole::variable
| HighlightRole::mathvar
| HighlightRole::mathnum
| HighlightRole::matherr
| HighlightRole::assign
| HighlightRole::assign_array_bracket
| HighlightRole::here_string_tri
| HighlightRole::here_string_text
| HighlightRole::single_sq_bracket
| HighlightRole::double_sq_bracket
| HighlightRole::double_paren
| HighlightRole::bracket_level_1
| HighlightRole::bracket_level_2
| HighlightRole::bracket_level_3
| HighlightRole::case_input
| HighlightRole::case_parentheses
| HighlightRole::case_condition
| HighlightRole::globbing_ext
| HighlightRole::here_string_var
| HighlightRole::for_loop_variable
| HighlightRole::for_loop_operator
| HighlightRole::for_loop_number
| HighlightRole::for_loop_separator
| HighlightRole::correct_subtle
| HighlightRole::incorrect_subtle => role,
HighlightRole::pager_secondary_background => HighlightRole::pager_background,
HighlightRole::pager_secondary_prefix | HighlightRole::pager_selected_prefix => {
HighlightRole::pager_prefix
}
HighlightRole::pager_secondary_completion | HighlightRole::pager_selected_completion => {
HighlightRole::pager_completion
}
HighlightRole::pager_secondary_description | HighlightRole::pager_selected_description => {
HighlightRole::pager_description
}
HighlightRole::pager_selected_background => HighlightRole::search_match,
}
}
fn parse_style_for_highlight(spec: &str) -> Option<zattr> {
if spec.is_empty() {
return None;
}
let (mask_on, _mask_off) = match_highlight(spec);
Some(mask_on)
}
fn zsh_highlight_styles_get(key: &str) -> Option<String> {
let keys = gethkparam("ZSH_HIGHLIGHT_STYLES")?;
let vals = gethparam("ZSH_HIGHLIGHT_STYLES")?;
keys.iter()
.zip(vals.iter())
.find(|(k, _)| k.as_str() == key)
.map(|(_, v)| v.clone())
}
#[derive(Default)]
pub struct HighlightColorResolver {
cache: HashMap<HighlightSpec, zattr>,
}
impl HighlightColorResolver {
pub fn new() -> Self {
Default::default()
}
pub fn resolve_spec(&mut self, highlight: &HighlightSpec) -> zattr {
match self.cache.entry(*highlight) {
Entry::Occupied(e) => *e.get(),
Entry::Vacant(e) => {
let face = Self::resolve_spec_uncached(highlight);
e.insert(face);
face
}
}
}
pub fn resolve_spec_uncached(highlight: &HighlightSpec) -> zattr {
let secondary = highlight.secondary;
let resolve_role = |role: HighlightRole| -> zattr {
let mut roles: &[HighlightRole] = &[role, get_fallback(role), HighlightRole::normal];
for i in [2, 1] {
if roles[i - 1] == roles[i] {
roles = &roles[..i];
}
}
for &role in roles {
let configured = if role == HighlightRole::autosuggestion {
getsparam("ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE").filter(|s| !s.is_empty())
} else if secondary {
zsh_highlight_styles_get(&format!(
"secondary-{}",
get_highlight_style_key(role)
))
} else {
zsh_highlight_styles_get(get_highlight_style_key(role))
};
if let Some(face) = configured.as_deref().and_then(parse_style_for_highlight) {
return face;
}
}
let spec = if secondary {
get_secondary_default_style(role)
} else {
get_default_style(role)
};
parse_style_for_highlight(spec).unwrap_or(0)
};
let mut face = resolve_role(highlight.foreground);
if highlight.background != highlight.foreground {
use crate::ported::zsh_h::{TXTBGCOLOUR, TXT_ATTR_BG_COL_MASK};
let bg_face = resolve_role(highlight.background);
face |= bg_face & (TXTBGCOLOUR as zattr | TXT_ATTR_BG_COL_MASK);
}
if highlight.valid_path {
let path_spec = zsh_highlight_styles_get("path").unwrap_or_default();
let merged = if path_spec.is_empty() {
parse_style_for_highlight("underline")
} else {
parse_style_for_highlight(&path_spec)
};
if let Some(m) = merged {
face |= m;
}
}
if highlight.force_underline {
face |= crate::ported::zsh_h::TXTUNDERLINE as zattr;
}
face
}
}
pub fn colorize(text: &str, colors: &[HighlightSpec]) -> Vec<u8> {
let chars: Vec<char> = text.chars().collect();
assert_eq!(colors.len(), chars.len());
let mut rv = HighlightColorResolver::new();
let mut out: Vec<u8> = Vec::new();
let mut last_color: Option<HighlightSpec> = None;
for (i, &c) in chars.iter().enumerate() {
let color = colors[i];
if Some(color) != last_color {
let face = rv.resolve_spec(&color);
out.extend_from_slice(zattr_to_sgr(face).as_bytes());
last_color = Some(color);
}
if i + 1 == chars.len() && c == '\n' {
out.extend_from_slice(b"\x1b[0m");
}
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
out.extend_from_slice(b"\x1b[0m"); out
}
fn zattr_to_sgr(attr: zattr) -> String {
use crate::ported::zsh_h::{
TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR, TXTSTANDOUT, TXTUNDERLINE, TXT_ATTR_BG_COL_SHIFT,
TXT_ATTR_FG_COL_SHIFT,
};
let mut s = String::from("\x1b[0");
if attr & TXTBOLDFACE as zattr != 0 {
s.push_str(";1");
}
if attr & TXTSTANDOUT as zattr != 0 {
s.push_str(";7");
}
if attr & TXTUNDERLINE as zattr != 0 {
s.push_str(";4");
}
if attr & TXTFGCOLOUR as zattr != 0 {
let col = (attr >> TXT_ATTR_FG_COL_SHIFT) & 0xffffff;
s.push_str(&format!(";38;5;{}", col));
}
if attr & TXTBGCOLOUR as zattr != 0 {
let col = (attr >> TXT_ATTR_BG_COL_SHIFT) & 0xffffff;
s.push_str(&format!(";48;5;{}", col));
}
s.push('m');
s
}
pub fn highlight_shell(
buff: &str,
color: &mut Vec<HighlightSpec>,
ctx: &OperationContext,
io_ok: bool,
cursor: Option<usize>,
) {
let working_directory = getsparam("PWD").unwrap_or_else(|| ".".to_owned());
let mut highlighter = Highlighter::new(buff, cursor, ctx, working_directory, io_ok);
*color = highlighter.highlight();
}
pub fn highlight_and_colorize(text: &str, ctx: &OperationContext) -> Vec<u8> {
let mut colors = Vec::new();
highlight_shell(
text,
&mut colors,
ctx,
false,
None,
);
colorize(text, &colors)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum StatementDecoration {
#[default]
None_,
Command, Builtin, Exec,
}
static CMD_VALID_CACHE: Mutex<Option<(String, HashMap<(String, u8), bool>)>> = Mutex::new(None);
const CMD_VALID_CACHE_MAX: usize = 8192;
pub fn command_is_valid_cached(
cmd: &str,
decoration: StatementDecoration,
working_directory: &str,
) -> bool {
let path_now = getsparam("PATH").unwrap_or_default();
let key = (cmd.to_owned(), decoration as u8);
let cached: Option<bool> = {
let mut guard = CMD_VALID_CACHE.lock().unwrap();
match guard.as_mut() {
Some((path, map)) if *path == path_now => map.get(&key).copied(),
_ => {
*guard = Some((path_now, HashMap::new()));
None
}
}
};
let tables_valid = cached.unwrap_or_else(|| {
let v = command_is_valid_tables(cmd, decoration);
let mut guard = CMD_VALID_CACHE.lock().unwrap();
if let Some((_, map)) = guard.as_mut() {
if map.len() >= CMD_VALID_CACHE_MAX {
map.clear();
}
map.insert(key, v);
}
v
});
if tables_valid {
return true;
}
if decoration == StatementDecoration::None_ && isset(AUTOCD) {
let path = crate::zle_file_tester::path_apply_working_directory(cmd, working_directory);
return std::fs::metadata(&path)
.map(|m| m.is_dir())
.unwrap_or(false);
}
false
}
pub fn command_is_valid(
cmd: &str,
decoration: StatementDecoration,
working_directory: &str,
) -> bool {
if command_is_valid_tables(cmd, decoration) {
return true;
}
if decoration == StatementDecoration::None_ && isset(AUTOCD) {
let path = crate::zle_file_tester::path_apply_working_directory(cmd, working_directory);
if std::fs::metadata(&path)
.map(|m| m.is_dir())
.unwrap_or(false)
{
return true;
}
}
false
}
fn command_is_valid_tables(cmd: &str, decoration: StatementDecoration) -> bool {
let mut builtin_ok = true;
let mut function_ok = true;
let mut alias_ok = true;
let mut command_ok = true;
if matches!(
decoration,
StatementDecoration::Command | StatementDecoration::Exec
) {
builtin_ok = false;
function_ok = false;
alias_ok = false;
command_ok = true;
} else if decoration == StatementDecoration::Builtin {
builtin_ok = true;
function_ok = false;
alias_ok = false;
command_ok = false;
}
let mut is_valid = false;
if !is_valid && builtin_ok {
is_valid = crate::ported::builtin::createbuiltintable().contains_key(cmd)
|| (crate::ext_builtins::EXT_BUILTIN_NAMES.contains(&cmd)
&& crate::ext_builtins::builtin_in_builtintab(cmd));
}
if !is_valid && function_ok {
is_valid = getshfunc(cmd).is_some();
}
if !is_valid && alias_ok {
is_valid = aliastab_lock()
.read()
.map(|t| t.get(cmd).is_some())
.unwrap_or(false);
}
if !is_valid && command_ok {
is_valid = cmdnamtab_lock()
.read()
.map(|t| t.get(cmd).is_some())
.unwrap_or(false)
|| findcmd(cmd, 0, 0).is_some();
}
is_valid
}
fn math_name_exists(name: &str) -> bool {
crate::ported::params::paramtab()
.read()
.map(|t| t.get(name).is_some())
.unwrap_or(false)
}
fn fsh_expand_for_path(tokenized: &str) -> Option<String> {
use crate::ported::zsh_h::{Bnull, Bnullkeep, Comma, Dash, Dnull, Nularg, Snull, Tilde};
let mut s = tokenized.to_owned();
if let Some(rest) = s.strip_prefix(crate::ported::zsh_h::Equals) {
return crate::ported::exec::findcmd(rest, 0, 0);
}
if s.starts_with(Tilde) {
crate::zle_file_tester::expand_tilde_quiet(&mut s);
}
let mut out = String::new();
for c in s.chars() {
match c {
Snull | Dnull | Bnull | Bnullkeep | Nularg => (),
Tilde => out.push('~'),
Comma => out.push(','),
Dash => out.push('-'),
c if ('\u{84}'..='\u{a1}').contains(&c) => return None,
c => out.push(c),
}
}
if out.is_empty() {
None
} else {
Some(out)
}
}
#[derive(Clone, Copy, PartialEq)]
pub enum Chroma {
Autoload, Source, Printf, }
impl Chroma {
fn for_command(cmd: &str) -> Option<Chroma> {
match cmd {
"autoload" => Some(Chroma::Autoload),
"source" | "." => Some(Chroma::Source),
"printf" => Some(Chroma::Printf),
_ => None,
}
}
}
fn printf_conversions(w: &[char]) -> Vec<(usize, usize)> {
const FLAGS: &str = "#+ 0-";
const CONV: &str = "diouxXfFeEgGaAcsb";
let mut out = Vec::new();
let mut i = 0usize;
while i < w.len() {
if w[i] != '%' {
i += 1;
continue;
}
let start = i;
let mut j = i + 1;
while j < w.len() && FLAGS.contains(w[j]) {
j += 1;
}
while j < w.len() && w[j].is_ascii_digit() {
j += 1;
}
if j < w.len() && w[j] == '.' {
j += 1;
while j < w.len() && w[j].is_ascii_digit() {
j += 1;
}
}
if j < w.len() && CONV.contains(w[j]) {
out.push((start, j + 1));
i = j + 1;
} else {
i = start + 1;
}
}
out
}
fn fsh_has_quote(tokenized: &str) -> bool {
use crate::ported::zsh_h::{Dnull, Snull};
tokenized.chars().any(|c| c == Snull || c == Dnull)
}
fn fsh_is_globbing_ext(word: &str) -> bool {
if word.contains("(#b)") || word.contains("(#B)") || word.contains("(#m)") || word.contains("(#c")
{
return true;
}
let c: Vec<char> = word.chars().collect();
(1..c.len()).any(|i| c[i] == '#' && c[i - 1] == '#' && (i < 2 || c[i - 2] != '\\'))
}
fn fsh_afp_option(word: &str, set: &str) -> bool {
let Some(rest) = word.strip_prefix('-') else {
return false;
};
!rest.is_empty() && rest.chars().all(|c| set.contains(c))
}
pub fn fsh_is_precommand(word: &str) -> bool {
matches!(
word,
"builtin" | "command" | "exec" | "nocorrect" | "noglob" | "pkexec" | "sudo" | "doas"
)
}
fn has_expand_reserved(s: &str) -> bool {
s.chars().any(|wc| ('\u{84}'..='\u{a1}').contains(&wc))
}
pub fn autosuggest_parse_command(buff: &str) -> Option<(String, String)> {
let toks = lex_line_tokens(buff);
let mut cmd: Option<String> = None;
let mut arg = String::new(); for t in &toks {
if t.tok == STRING_LEX {
match &cmd {
None if t.cmdpos => {
let text = t.clean_text();
let mut expanded = t.text.clone().unwrap_or_default();
if expand_one_no_cmdsubst(&mut expanded) && !expanded.is_empty() {
cmd = Some(expanded);
} else {
cmd = Some(text);
}
}
None => (),
Some(_) => {
if !t.in_redir {
arg = t.clean_text();
}
break;
}
}
} else if cmd.is_some() {
break; }
}
cmd.map(|c| (c, arg)) }
pub fn is_veritable_cd(expanded_command: &str) -> bool {
expanded_command == "cd"
&& aliastab_lock()
.read()
.map(|t| t.get("cd").is_none())
.unwrap_or(true)
}
pub fn autosuggest_validate_from_history(
item_commandline: &str,
required_paths: &[String],
working_directory: &str,
ctx: &OperationContext,
) -> bool {
let Some((parsed_command, mut cd_dir)) = autosuggest_parse_command(item_commandline) else {
return true;
};
if is_veritable_cd(&parsed_command) && !cd_dir.is_empty() {
if expand_one_no_cmdsubst(&mut cd_dir) {
if "--help".starts_with(&cd_dir) || "-h".starts_with(&cd_dir) {
return true;
} else {
return crate::zle_file_tester::is_potential_cd_path(
&cd_dir,
false,
working_directory,
ctx,
Default::default(),
);
}
}
}
let cmd_ok = command_is_valid_cached(
&parsed_command,
StatementDecoration::None_,
working_directory,
);
if !cmd_ok {
return false;
}
if !required_paths.is_empty() {
let tester = FileTester::new(working_directory.to_owned(), ctx);
if !required_paths.iter().all(|p| tester.test_path(p, false)) {
return false;
}
}
true }
fn valid_var_name_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn valid_var_name(s: &str) -> bool {
!s.is_empty()
&& !s.starts_with(|c: char| c.is_ascii_digit())
&& s.chars().all(valid_var_name_char)
}
fn is_special_param_char(c: char) -> bool {
matches!(c, '?' | '#' | '$' | '!' | '@' | '*' | '-' | '_') || c.is_ascii_digit()
}
fn color_variable(inp: &[char], colors: &mut [HighlightSpec], role: HighlightRole) -> usize {
assert_eq!(inp[0], '$');
let at = |i: usize| -> char { inp.get(i).copied().unwrap_or('\0') };
let mut idx = 0;
let mut dollar_count = 0;
while at(idx) == '$' {
let next = at(idx + 1);
if next == '$' || valid_var_name_char(next) || is_special_param_char(next) {
colors[idx] = HighlightSpec::with_fg(role);
} else if next == '(' || next == '{' || next == '\'' {
colors[idx] = HighlightSpec::with_fg(role);
return idx + 1;
} else {
colors[idx] = HighlightSpec::with_fg(HighlightRole::error);
}
idx += 1;
dollar_count += 1;
}
if idx == dollar_count && !valid_var_name_char(at(idx)) && is_special_param_char(at(idx)) {
colors[idx] = HighlightSpec::with_fg(role);
return idx + 1;
}
loop {
if valid_var_name_char(at(idx)) {
colors[idx] = HighlightSpec::with_fg(role);
idx += 1;
} else if at(idx) == '\\' && at(idx + 1) == '\n' {
colors[idx] = HighlightSpec::with_fg(role);
idx += 1;
colors[idx] = HighlightSpec::with_fg(role);
idx += 1;
} else {
break;
}
}
for _slice_count in 0..dollar_count {
match subscript_length(&inp[idx..]) {
Some(slice_len) if slice_len > 0 => {
colors[idx] = HighlightSpec::with_fg(role);
colors[idx + slice_len - 1] = HighlightSpec::with_fg(role);
idx += slice_len;
}
Some(_slice_len) => {
break;
}
None => {
colors[..=idx].fill(HighlightSpec::with_fg(HighlightRole::error));
break;
}
}
}
idx
}
fn subscript_length(inp: &[char]) -> Option<usize> {
if inp.first() != Some(&'[') {
return Some(0);
}
let mut depth = 0usize;
for (i, &c) in inp.iter().enumerate() {
match c {
'[' => depth += 1,
']' => {
depth -= 1;
if depth == 0 {
return Some(i + 1);
}
}
_ => (),
}
}
None
}
pub fn color_string_internal(
buffstr: &str,
base_color: HighlightSpec,
colors: &mut [HighlightSpec],
) {
assert!(
!matches!(
base_color.foreground,
HighlightRole::quote
| HighlightRole::dquoted
| HighlightRole::dollar_quoted
| HighlightRole::escape
),
"Unexpected base color"
);
let chars: Vec<char> = buffstr.chars().collect();
let buff_len = chars.len();
colors.fill(base_color);
#[derive(Eq, PartialEq)]
#[allow(dead_code)]
enum Mode {
unquoted,
single_quoted,
double_quoted,
dollar_quoted, backtick, }
let mut mode = Mode::unquoted;
let mut unclosed_quote_offset = None;
let mut bracket_count = 0;
let mut in_pos = 0;
while in_pos < buff_len {
let c = chars[in_pos];
match mode {
Mode::unquoted => {
if c == '\\' {
in_pos += 1; } else {
match c {
'$' if chars.get(in_pos + 1) == Some(&'\'') => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::dollar_quoted);
colors[in_pos + 1] =
HighlightSpec::with_fg(HighlightRole::dollar_quoted);
unclosed_quote_offset = Some(in_pos);
in_pos += 1;
mode = Mode::dollar_quoted;
}
'`' => {
unclosed_quote_offset = Some(in_pos);
mode = Mode::backtick;
}
'\'' => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote);
unclosed_quote_offset = Some(in_pos);
mode = Mode::single_quoted;
}
'"' => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::dquoted);
unclosed_quote_offset = Some(in_pos);
mode = Mode::double_quoted;
}
_ => (), }
}
}
Mode::single_quoted => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote);
if c == '\'' {
mode = Mode::unquoted;
}
}
Mode::double_quoted => {
if colors[in_pos] == base_color {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::dquoted);
}
match c {
'"' => {
mode = Mode::unquoted;
}
'\\' if in_pos + 1 < buff_len => {
let escaped_char = chars[in_pos + 1];
if matches!(escaped_char, '\\' | '"' | '$' | '`' | '\n') {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape);
colors[in_pos + 1] = HighlightSpec::with_fg(HighlightRole::escape);
in_pos += 1; }
}
'$' => {
in_pos += color_variable(
&chars[in_pos..],
&mut colors[in_pos..],
HighlightRole::dollar_in_dquote,
);
in_pos -= 1;
}
_ => (), }
}
Mode::dollar_quoted => {
colors[in_pos] = HighlightSpec::with_fg(HighlightRole::dollar_quoted);
if c == '\\' && in_pos + 1 < buff_len {
let mut fill_color = HighlightRole::escape;
let backslash_pos = in_pos;
let mut fill_end = backslash_pos;
in_pos += 1;
let escaped_char = chars[in_pos];
if "abcefnrtv\\'\"?".contains(escaped_char) {
fill_end = in_pos + 1;
} else if "uUxX01234567".contains(escaped_char) {
let mut res: u32 = 0;
let mut chars_max = 2;
let mut base = 16;
let mut max_val = 0x7f_u32;
match escaped_char {
'u' => {
chars_max = 4;
max_val = 0xFFFF; in_pos += 1;
}
'U' => {
chars_max = 8;
max_val = 0x10FFFF;
in_pos += 1;
}
'x' | 'X' => {
max_val = 0xFF;
in_pos += 1;
}
_ => {
base = 8;
chars_max = 3;
}
}
let first_digit = in_pos;
for _i in 0..chars_max {
if in_pos == buff_len {
break;
}
let Some(d) = chars[in_pos].to_digit(base) else {
break;
};
res = res.saturating_mul(base).saturating_add(d);
in_pos += 1;
}
fill_end = in_pos;
let _ = max_val;
if res == 0 && in_pos == first_digit && matches!(escaped_char, 'u' | 'U' | 'x' | 'X') {
fill_color = HighlightRole::error;
}
in_pos -= 1;
} else {
fill_end = in_pos + 1;
}
if fill_end > backslash_pos {
colors[backslash_pos..fill_end.min(buff_len)]
.fill(HighlightSpec::with_fg(fill_color));
} else {
colors[backslash_pos] = HighlightSpec::with_fg(fill_color);
colors[in_pos.min(buff_len - 1)] = HighlightSpec::with_fg(fill_color);
}
} else if c == '\'' {
mode = Mode::unquoted;
}
}
Mode::backtick => {
if c == '`' {
mode = Mode::unquoted;
}
}
}
in_pos += 1;
}
let _ = (mode, unclosed_quote_offset);
}
#[derive(Clone, Debug)]
pub struct TokSpan {
pub tok: lextok,
pub text: Option<String>,
pub start: usize,
pub end: usize,
pub cmdpos: bool,
pub in_redir: bool,
}
impl TokSpan {
pub fn clean_text(&self) -> String {
let mut s = self.text.clone().unwrap_or_default();
crate::ported::glob::remnulargs(&mut s);
untokenize(&s)
}
}
pub fn lex_line_tokens(line: &str) -> Vec<TokSpan> {
use crate::ported::zle::compcore::{ADDEDX, ZLEMETACS, ZLEMETALL};
use std::sync::atomic::Ordering;
let ll = line.chars().count() as i32;
let mut out: Vec<TokSpan> = Vec::new();
let saved_cs = ZLEMETACS.load(Ordering::SeqCst);
let saved_ll = ZLEMETALL.load(Ordering::SeqCst);
let saved_addedx = ADDEDX.load(Ordering::SeqCst);
let saved_noaliases = crate::ported::lex::noaliases();
crate::ported::lex::set_noaliases(true);
crate::ported::context::zcontext_save(); let saved_unget: std::collections::VecDeque<char> =
crate::ported::lex::LEX_UNGET_BUF.with_borrow_mut(std::mem::take);
ZLEMETALL.store(ll, Ordering::SeqCst);
ZLEMETACS.store(ll + 5, Ordering::SeqCst); ADDEDX.store(0, Ordering::SeqCst);
LEX_LEXFLAGS.set(LEXFLAGS_ZLE | LEXFLAGS_ACTIVE);
crate::ported::input::inpush(&crate::ported::zle::zle_tricky::dupstrspace(line), 0, None); crate::ported::hist::strinbeg(0);
let mut prev_inbufct = i32::MIN;
loop {
let cmdpos_before = incmdpos();
let inredir_before = inredir();
ctxtlex();
let mut tokv = tok();
let was_lexerr = tokv == LEXERR;
if tokv == LEXERR {
match tokstr() {
None => break,
Some(ts) => {
use crate::ported::zsh_h::{Dnull, Snull};
let jcnt = ts.chars().filter(|&c| c == Snull || c == Dnull).count();
if jcnt & 1 == 1 {
tokv = STRING_LEX;
}
}
}
}
if tokv == ENDINPUT {
break; }
let inbufct = crate::ported::input::inbufct.with(|c| c.get());
let wordbeg = LEX_WORDBEG.get();
let start = (ll - wordbeg).clamp(0, ll) as usize; let end = (ll + 1 - inbufct).clamp(start as i32, ll) as usize;
let no_progress = inbufct == prev_inbufct;
prev_inbufct = inbufct;
out.push(TokSpan {
tok: tokv,
text: tokstr(),
start,
end,
cmdpos: cmdpos_before,
in_redir: inredir_before,
});
if (was_lexerr && no_progress) || out.len() > (ll as usize + 8) {
break;
}
if was_lexerr && inbufct <= 1 {
break; }
}
crate::ported::hist::strinend(); crate::ported::input::inpop(); crate::ported::context::zcontext_restore(); crate::ported::lex::LEX_UNGET_BUF.with_borrow_mut(|b| *b = saved_unget);
crate::ported::lex::set_noaliases(saved_noaliases);
ZLEMETACS.store(saved_cs, Ordering::SeqCst);
ZLEMETALL.store(saved_ll, Ordering::SeqCst);
ADDEDX.store(saved_addedx, Ordering::SeqCst);
out
}
pub struct Highlighter<'s> {
buff: &'s str,
buff_chars: Vec<char>,
cursor: Option<usize>,
ctx: &'s OperationContext,
io_ok: bool,
working_directory: String,
file_tester: FileTester<'s>,
color_array: ColorArray,
pending_variables: Vec<String>,
complex_brackets: Vec<usize>,
done: bool,
}
impl<'s> Highlighter<'s> {
pub fn new(
buff: &'s str,
cursor: Option<usize>,
ctx: &'s OperationContext,
working_directory: String,
can_do_io: bool,
) -> Self {
let file_tester = FileTester::new(working_directory.clone(), ctx);
Self {
buff,
buff_chars: buff.chars().collect(),
cursor,
ctx,
io_ok: can_do_io,
working_directory,
file_tester,
color_array: vec![],
pending_variables: vec![],
complex_brackets: vec![],
done: false,
}
}
pub fn highlight(&mut self) -> ColorArray {
assert!(!self.done);
self.done = true;
self.color_array
.resize(self.buff_chars.len(), HighlightSpec::default());
let toks = lex_line_tokens(self.buff);
self.visit_tokens(&toks);
if self.ctx.check_cancel() {
return std::mem::take(&mut self.color_array);
}
if isset(INTERACTIVECOMMENTS) {
self.color_gap_comments(&toks);
}
for t in toks.iter().filter(|t| t.tok == LEXERR) {
self.color_span(t.start, self.buff_chars.len(), HighlightRole::error);
}
self.highlight_brackets();
std::mem::take(&mut self.color_array)
}
fn io_still_ok(&self) -> bool {
self.io_ok && !self.ctx.check_cancel()
}
fn color_span(&mut self, start: usize, end: usize, role: HighlightRole) {
let end = end.min(self.color_array.len());
if start < end {
self.color_array[start..end].fill(HighlightSpec::with_fg(role));
}
}
fn span_text(&self, t: &TokSpan) -> String {
self.buff_chars[t.start.min(self.buff_chars.len())..t.end.min(self.buff_chars.len())]
.iter()
.collect()
}
fn visit_tokens(&mut self, toks: &[TokSpan]) {
let mut decoration = StatementDecoration::None_;
let mut after_decoration = false;
let mut expanded_cmd = String::new();
let mut is_cd = false;
let mut is_typeset = false;
let mut have_dashdash = false;
let mut highlight_glob = true;
let mut here_string_word = false;
let mut in_array_assignment = false;
let mut name_word = false;
let mut for_mode = false;
#[derive(PartialEq, Clone, Copy)]
enum CaseState {
None_,
Subject,
In,
Pattern,
Body,
}
let mut case_state = CaseState::None_;
let mut sudo_opt = false;
let mut sudo_arg = false;
let mut afp: Option<String> = None;
let mut repeat_count = false;
let mut soft_unknown = false;
let mut in_cond = false;
let mut after_close_brace = false;
let mut eval_mode = false;
let mut chroma: Option<Chroma> = None;
let mut chroma_words = 0usize;
let mut chroma_skip = false;
let mut i = 0;
while i < toks.len() {
if self.ctx.check_cancel() {
return;
}
let t = &toks[i];
let tokv = t.tok;
if IS_REDIROP(tokv) {
if tokv == TRINANG {
self.color_span(t.start, t.end, HighlightRole::here_string_tri);
here_string_word = true;
i += 1;
continue;
}
let target = toks.get(i + 1).filter(|n| n.tok == STRING_LEX);
self.visit_redirection(t, target);
if target.is_some() {
i += 2;
} else {
i += 1;
}
continue;
}
match tokv {
SEPER | NEWLIN | SEMI | AMPER | AMPERBANG | BAR_TOK | BARAMP | DBAR | DAMPER => {
self.color_span(t.start, t.end, HighlightRole::statement_terminator);
decoration = StatementDecoration::None_;
expanded_cmd.clear();
is_cd = false;
is_typeset = false;
have_dashdash = false;
highlight_glob = true; name_word = false;
for_mode = false; sudo_opt = false;
sudo_arg = false;
afp = None;
repeat_count = false;
soft_unknown = false;
after_close_brace = false;
eval_mode = false;
chroma = None;
chroma_words = 0;
chroma_skip = false;
}
DSEMI | SEMIAMP | SEMIBAR => {
if case_state == CaseState::Body {
case_state = CaseState::Pattern;
}
expanded_cmd.clear();
is_cd = false;
is_typeset = false;
have_dashdash = false;
highlight_glob = true;
}
INPAR_TOK | OUTPAR_TOK => {
if tokv == OUTPAR_TOK && in_array_assignment {
in_array_assignment = false;
self.color_span(t.start, t.end, HighlightRole::assign_array_bracket);
self.complex_brackets.push(t.start);
} else if tokv == OUTPAR_TOK && case_state == CaseState::Pattern {
case_state = CaseState::Body;
after_decoration = true;
self.color_span(t.start, t.end, HighlightRole::case_parentheses);
self.complex_brackets.push(t.start);
} else {
self.color_span(t.start, t.end, HighlightRole::keyword);
}
}
ENVSTRING => {
self.visit_variable_assignment(t);
}
ENVARRAY => {
self.visit_variable_assignment(t);
self.mark_assign_parens(t, &mut in_array_assignment);
}
STRING_LEX if here_string_word => {
here_string_word = false;
self.color_span(t.start, t.end, HighlightRole::here_string_text);
let n = self.color_array.len();
let (lo, hi) = (t.start.min(n), t.end.min(n));
let body: Vec<char> = self.buff_chars[lo..hi].to_vec();
let mut k = 0usize;
while k < body.len() {
if body[k] == '$' {
k += color_variable(
&body[k..],
&mut self.color_array[lo + k..hi],
HighlightRole::here_string_var,
);
} else {
k += 1;
}
}
}
STRING_LEX if case_state == CaseState::Subject => {
case_state = CaseState::In;
self.color_span(t.start, t.end, HighlightRole::case_input);
}
STRING_LEX if case_state == CaseState::In => {
case_state = CaseState::Pattern;
self.color_span(t.start, t.end, HighlightRole::keyword);
}
STRING_LEX if case_state == CaseState::Pattern => {
let n = self.color_array.len();
let (lo, hi) = (t.start.min(n), t.end.min(n));
self.color_array[lo..hi]
.fill(HighlightSpec::with_bg(HighlightRole::case_condition));
}
STRING_LEX if for_mode || name_word => {
name_word = false;
self.visit_argument(t, false, true, highlight_glob);
}
STRING_LEX if repeat_count => {
repeat_count = false;
soft_unknown = true;
after_decoration = true;
}
STRING_LEX if sudo_arg => {
sudo_arg = false;
sudo_opt = true;
after_decoration = true;
}
STRING_LEX if sudo_opt && t.clean_text().starts_with('-') => {
let clean = t.clean_text();
self.color_span(
t.start,
t.end,
if clean.starts_with("--") {
HighlightRole::double_hyphen_option
} else {
HighlightRole::option
},
);
if matches!(clean.as_str(), "-C" | "-g" | "-p" | "-r" | "-t" | "-u") {
sudo_arg = true;
sudo_opt = false;
}
after_decoration = true;
}
STRING_LEX
if afp.as_deref() == Some("command")
&& fsh_afp_option(&t.clean_text(), "pvV-") =>
{
self.color_span(t.start, t.end, HighlightRole::option);
after_decoration = true;
}
STRING_LEX
if afp.as_deref() == Some("exec") && fsh_afp_option(&t.clean_text(), "cla-") =>
{
self.color_span(t.start, t.end, HighlightRole::option);
after_decoration = true;
}
STRING_LEX if after_close_brace && t.clean_text() == "always" => {
self.color_span(t.start, t.end, HighlightRole::keyword);
after_decoration = true;
}
STRING_LEX if after_close_brace && t.clean_text() == "{" => {
after_close_brace = false;
self.color_span(t.start, t.end, HighlightRole::keyword);
expanded_cmd.clear();
after_decoration = true;
}
STRING_LEX => {
after_close_brace = false;
if in_cond {
self.visit_argument(t, false, true, highlight_glob);
} else if (t.cmdpos || after_decoration) && expanded_cmd.is_empty() {
let clean = t.clean_text();
if clean == "noglob" {
highlight_glob = false; }
if toks.get(i + 1).map(|n| n.tok) == Some(INOUTPAR) {
after_decoration = false;
} else if fsh_is_precommand(&clean) {
decoration = match clean.as_str() {
"command" => StatementDecoration::Command,
"builtin" => StatementDecoration::Builtin,
"exec" => StatementDecoration::Exec,
_ => decoration,
};
if matches!(clean.as_str(), "sudo" | "doas") {
sudo_opt = true; }
if matches!(clean.as_str(), "command" | "exec") {
afp = Some(clean.clone()); }
self.color_span(t.start, t.end, HighlightRole::precommand);
after_decoration = true;
} else {
let soft = std::mem::take(&mut soft_unknown);
self.visit_command_word_soft(t, &clean, decoration, soft);
after_decoration = false;
expanded_cmd = clean;
is_cd = is_veritable_cd(&expanded_cmd);
eval_mode = expanded_cmd == "eval"; chroma = Chroma::for_command(&expanded_cmd);
chroma_words = 0;
chroma_skip = false;
is_typeset = matches!(
expanded_cmd.as_str(),
"typeset"
| "local"
| "declare"
| "export"
| "readonly"
| "integer"
| "float"
);
}
} else {
if is_typeset {
let arg = t.clean_text();
let name = arg.split('=').next().unwrap_or("").to_owned();
if valid_var_name(&name) {
self.pending_variables.push(name);
}
}
if let Some(c) = chroma {
if self.visit_chroma_argument(t, c, &mut chroma_words, &mut chroma_skip) {
i += 1;
continue;
}
self.visit_argument(t, is_cd, !have_dashdash, highlight_glob);
} else if eval_mode && self.visit_eval_argument(t) {
} else if is_typeset {
self.color_span(t.start, t.end, HighlightRole::assign);
self.mark_assign_parens(t, &mut in_array_assignment);
} else {
self.visit_argument(t, is_cd, !have_dashdash, highlight_glob);
}
if self.span_text(t) == "--" {
have_dashdash = true; }
}
}
DINBRACK | DOUTBRACK => {
self.color_span(t.start, t.end, HighlightRole::double_sq_bracket);
self.complex_brackets.push(t.start);
self.complex_brackets.push(t.start + 1);
in_cond = tokv == DINBRACK;
}
INOUTPAR => {
self.color_span(t.start, t.end, HighlightRole::keyword);
self.complex_brackets.push(t.start);
self.complex_brackets.push(t.start + 1);
expanded_cmd.clear();
after_decoration = true;
}
DINPAR | DOUTPAR => {
let n = self.buff_chars.len();
let mut body_lo = t.start.min(n);
let mut body_end = t.end.min(n);
if self.buff_chars.get(body_lo) == Some(&'(')
&& self.buff_chars.get(body_lo + 1) == Some(&'(')
{
self.color_span(body_lo, body_lo + 2, HighlightRole::double_paren);
self.complex_brackets.push(body_lo);
self.complex_brackets.push(body_lo + 1);
body_lo += 2;
}
{
let closer = if body_end >= body_lo + 2
&& self.buff_chars[body_end - 2] == ')'
&& self.buff_chars[body_end - 1] == ')'
{
body_end -= 2;
Some(body_end)
} else if self.buff_chars.get(body_end) == Some(&')')
&& self.buff_chars.get(body_end + 1) == Some(&')')
{
Some(body_end)
} else {
None
};
if for_mode {
self.highlight_for_loop(body_lo, body_end);
} else {
self.highlight_math(body_lo, body_end);
}
if let Some(c) = closer {
self.color_span(c, c + 2, HighlightRole::double_paren);
self.complex_brackets.push(c);
self.complex_brackets.push(c + 1);
}
}
}
INBRACE_TOK | OUTBRACE_TOK => {
self.color_span(t.start, t.end, HighlightRole::keyword);
expanded_cmd.clear();
is_typeset = false;
after_decoration = tokv == INBRACE_TOK;
after_close_brace = tokv == OUTBRACE_TOK;
}
BANG_TOK => {
self.color_span(t.start, t.end, HighlightRole::keyword);
after_decoration = true;
}
tokv if (CASE..=TYPESET).contains(&tokv) => {
let clean = t.clean_text();
let role = if fsh_is_precommand(&clean) {
after_decoration = true;
HighlightRole::precommand
} else {
self.classify_command_word(&clean)
.unwrap_or(HighlightRole::keyword)
};
self.color_span(t.start, t.end, role);
match clean.as_str() {
"for" | "foreach" | "select" => for_mode = true,
"case" => case_state = CaseState::Subject,
"esac" => case_state = CaseState::None_,
"function" => name_word = true,
"repeat" => repeat_count = true, _ => (),
}
if fsh_is_precommand(&clean) && matches!(clean.as_str(), "nocorrect" | "noglob")
{
}
if tokv == TYPESET {
is_typeset = true;
}
}
LEXERR => {
self.color_span(t.start, t.end, HighlightRole::error);
}
_ => {
}
}
i += 1;
}
}
fn classify_command_word(&self, word: &str) -> Option<HighlightRole> {
if word.is_empty() {
return None;
}
if let Ok(tab) = aliastab_lock().read() {
if let Some(node) = tab.get(word) {
use crate::ported::zsh_h::ALIAS_GLOBAL;
return Some(if node.node.flags & ALIAS_GLOBAL as i32 != 0 {
HighlightRole::global_alias
} else if word.len() > 1 && word[1..].contains('=') {
HighlightRole::error
} else {
HighlightRole::alias_
});
}
}
if getshfunc(word).is_some() {
return Some(HighlightRole::function_);
}
if crate::ported::builtin::createbuiltintable().contains_key(word)
|| (crate::ext_builtins::EXT_BUILTIN_NAMES.contains(&word)
&& crate::ext_builtins::builtin_in_builtintab(word))
{
return Some(if word == "[" {
HighlightRole::single_sq_bracket
} else {
HighlightRole::builtin_
});
}
if cmdnamtab_lock()
.read()
.map(|t| t.get(word).is_some())
.unwrap_or(false)
|| findcmd(word, 0, 0).is_some()
{
return Some(HighlightRole::command);
}
if let Some((_, ext)) = word.rsplit_once('.') {
if !ext.is_empty()
&& sufaliastab_lock()
.read()
.map(|t| t.get(ext).is_some())
.unwrap_or(false)
{
return Some(HighlightRole::suffix_alias);
}
}
if reswdtab_lock()
.read()
.map(|t| t.get(word).is_some())
.unwrap_or(false)
{
return Some(HighlightRole::keyword);
}
let dir = crate::zle_file_tester::path_apply_working_directory(word, &self.working_directory);
if std::fs::metadata(&dir).map(|m| m.is_dir()).unwrap_or(false) {
return Some(HighlightRole::path_to_dir);
}
None
}
fn visit_command_word_soft(
&mut self,
t: &TokSpan,
clean: &str,
decoration: StatementDecoration,
soft: bool,
) {
if !soft {
return self.visit_command_word(t, clean, decoration);
}
if let Some(role) = self.classify_command_word(clean) {
if role != HighlightRole::error {
self.color_span(t.start, t.end, role);
}
}
}
fn visit_command_word(&mut self, t: &TokSpan, clean: &str, _decoration: StatementDecoration) {
if !self.io_still_ok() {
self.color_span(t.start, t.end, HighlightRole::command);
return;
}
let mut expanded = t.text.clone().unwrap_or_default();
let expanded_ok = expand_one_no_cmdsubst(&mut expanded);
let cmd = if expanded_ok && !expanded.is_empty() {
expanded
} else {
clean.to_owned()
};
let histchar = crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
if histchar != 0 {
let mut it = clean.chars();
if it.next() == char::from_u32(histchar as u32) && it.next().is_some() {
self.color_span(t.start, t.end, HighlightRole::history_expansion);
return;
}
}
let role = if has_expand_reserved(&cmd) {
Some(HighlightRole::command)
} else {
self.classify_command_word(&cmd)
};
match role {
Some(role) => {
let start = t.start;
let end = t.end.min(self.color_array.len());
let src: String = self.span_text(t);
color_string_internal(
&src,
HighlightSpec::with_fg(role),
&mut self.color_array[start..end],
);
}
None => self.color_span(t.start, t.end, HighlightRole::error),
}
if role == Some(HighlightRole::single_sq_bracket) {
self.complex_brackets.push(t.start);
}
}
fn visit_argument(
&mut self,
t: &TokSpan,
cmd_is_cd: bool,
options_allowed: bool,
highlight_glob: bool,
) {
let start = t.start;
let end = t.end.min(self.color_array.len());
if start >= end {
return;
}
let src: String = self.span_text(t);
let clean = t.clean_text();
let tokenized = t.text.clone().unwrap_or_default();
if clean.starts_with("$((") {
self.color_span(start, start + 1, HighlightRole::dollar_in_dquote);
self.color_span(start + 1, start + 3, HighlightRole::double_paren);
self.complex_brackets.push(start + 1);
self.complex_brackets.push(start + 2);
let mut body_end = end;
if clean.ends_with("))") && end >= start + 5 {
body_end = end - 2;
self.color_span(body_end, end, HighlightRole::double_paren);
self.complex_brackets.push(body_end);
self.complex_brackets.push(body_end + 1);
}
self.highlight_math(start + 3, body_end);
return;
}
let base = if options_allowed && clean.starts_with("--") {
HighlightRole::double_hyphen_option
} else if options_allowed && clean.starts_with('-') {
HighlightRole::option
} else if fsh_has_quote(&tokenized) {
HighlightRole::param
} else if clean == "]" {
self.complex_brackets.push(t.start);
HighlightRole::single_sq_bracket
} else if let Some(role) = self.fsh_plain_word_role(&clean, &tokenized, highlight_glob) {
role
} else {
HighlightRole::param
};
color_string_internal(
&src,
HighlightSpec::with_fg(base),
&mut self.color_array[start..end],
);
let src_chars: Vec<char> = src.chars().collect();
let mut scan = 0usize;
while let Some((open, close)) = locate_cmdsubst_span(&src_chars, scan) {
let inner_start = open + 2;
let inner_end = close.unwrap_or(src_chars.len());
let nested = src_chars[inner_start..inner_end].contains(&'(');
if nested {
scan = inner_start;
continue;
}
if inner_end > inner_start {
let arg_cursor = self.cursor.map(|c| c.wrapping_sub(start + inner_start));
let inner_src: String = src_chars[inner_start..inner_end].iter().collect();
let mut cmdsub_highlighter = Highlighter::new(
&inner_src,
arg_cursor,
self.ctx,
self.working_directory.clone(),
self.io_still_ok(),
);
let mut subcolors = cmdsub_highlighter.highlight();
for c in subcolors.iter_mut() {
c.secondary = true;
}
let dst_lo = (start + inner_start).min(self.color_array.len());
let dst_hi = (start + inner_end).min(self.color_array.len());
for (k, sub) in subcolors.iter().take(dst_hi - dst_lo).enumerate() {
if sub.foreground != HighlightRole::normal
|| sub.background != HighlightRole::normal
{
self.color_array[dst_lo + k] = *sub;
}
}
}
scan = inner_end + 1;
if scan >= src_chars.len() {
break;
}
}
self.highlight_backticks(&src_chars, start);
let _ = cmd_is_cd; }
fn visit_chroma_argument(
&mut self,
t: &TokSpan,
c: Chroma,
words: &mut usize,
skip: &mut bool,
) -> bool {
let clean = t.clean_text();
if c == Chroma::Printf {
if clean.starts_with('-') {
*skip = clean == "-v";
return false;
}
if *skip {
*skip = false;
return false;
}
}
let opt_lead = match c {
Chroma::Autoload => clean.starts_with('-') || clean.starts_with('+'),
Chroma::Source => clean.starts_with('-'),
Chroma::Printf => false,
};
if opt_lead {
self.color_span(
t.start,
t.end,
if clean.starts_with("--") {
HighlightRole::double_hyphen_option
} else {
HighlightRole::option
},
);
return true;
}
*words += 1;
match c {
Chroma::Autoload => {
if clean.starts_with('$') || clean.starts_with('/') || clean.starts_with('`') {
return false;
}
let found = crate::ported::params::getaparam("fpath")
.unwrap_or_default()
.iter()
.any(|dir| std::path::Path::new(&format!("{dir}/{clean}")).exists());
self.color_span(
t.start,
t.end,
if found {
HighlightRole::correct_subtle
} else {
HighlightRole::incorrect_subtle
},
);
true
}
Chroma::Source => {
if *words != 1 {
return false;
}
let Some(path) = fsh_expand_for_path(&t.text.clone().unwrap_or_default()) else {
return false;
};
let abs = crate::zle_file_tester::path_apply_working_directory(
&path,
&self.working_directory,
);
if std::fs::metadata(&abs).map(|m| m.is_file()).unwrap_or(false) {
self.color_span(t.start, t.end, HighlightRole::correct_subtle);
return true;
}
false
}
Chroma::Printf => {
if *words != 1 {
return false;
}
let n = self.buff_chars.len();
let (lo, hi) = (t.start.min(n), t.end.min(n));
let q = self.buff_chars.get(lo).copied();
if q == Some('"') {
self.color_span(lo, hi, HighlightRole::dquoted);
} else if q == Some('\'') {
self.color_span(lo, hi, HighlightRole::quote);
}
for (a, b) in printf_conversions(&self.buff_chars[lo..hi]) {
self.color_span(lo + a, lo + b, HighlightRole::mathnum);
}
true
}
}
}
fn visit_eval_argument(&mut self, t: &TokSpan) -> bool {
let n = self.buff_chars.len();
let (lo, hi) = (t.start.min(n), t.end.min(n));
if hi <= lo + 1 {
return false;
}
let q = self.buff_chars[lo];
if (q != '\'' && q != '"') || self.buff_chars[hi - 1] != q {
return false;
}
let inner: String = self.buff_chars[lo + 1..hi - 1].iter().collect();
if inner.is_empty() {
return true;
}
let mut sub = Highlighter::new(
&inner,
None,
self.ctx,
self.working_directory.clone(),
self.io_still_ok(),
);
let mut subcolors = sub.highlight();
for c in subcolors.iter_mut() {
c.secondary = true;
}
for (k, s) in subcolors.iter().enumerate() {
if s.foreground != HighlightRole::normal || s.background != HighlightRole::normal {
self.color_array[lo + 1 + k] = *s;
}
}
true
}
fn highlight_backticks(&mut self, src_chars: &[char], start: usize) {
let mut i = 0usize;
let mut in_squote = false;
while i < src_chars.len() {
match src_chars[i] {
'\\' => i += 1,
'\'' => in_squote = !in_squote,
'`' if !in_squote => {
let body_lo = i + 1;
let mut j = body_lo;
while j < src_chars.len() && src_chars[j] != '`' {
if src_chars[j] == '\\' {
j += 1;
}
j += 1;
}
let body_hi = j.min(src_chars.len());
if body_hi > body_lo {
let inner: String = src_chars[body_lo..body_hi].iter().collect();
let mut sub = Highlighter::new(
&inner,
None,
self.ctx,
self.working_directory.clone(),
self.io_still_ok(),
);
let mut subcolors = sub.highlight();
for c in subcolors.iter_mut() {
c.secondary = true;
}
let dst_lo = (start + body_lo).min(self.color_array.len());
let dst_hi = (start + body_hi).min(self.color_array.len());
for (k, sub) in subcolors.iter().take(dst_hi - dst_lo).enumerate() {
if sub.foreground != HighlightRole::normal
|| sub.background != HighlightRole::normal
{
self.color_array[dst_lo + k] = *sub;
}
}
}
i = body_hi;
}
_ => (),
}
i += 1;
}
}
fn fsh_plain_word_role(
&self,
clean: &str,
src: &str,
highlight_glob: bool,
) -> Option<HighlightRole> {
if clean.is_empty() {
return None;
}
if fsh_is_globbing_ext(clean) {
return Some(if highlight_glob {
HighlightRole::globbing_ext
} else {
HighlightRole::param
});
}
{
use crate::ported::zsh_h::{Quest, Star};
if src.chars().any(|c| c == Star || c == Quest) {
return Some(if highlight_glob {
HighlightRole::globbing
} else {
HighlightRole::param
});
}
}
if clean.starts_with('$') {
return Some(HighlightRole::variable);
}
let histchar =
crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
if histchar != 0 {
let mut it = clean.chars();
if it.next() == char::from_u32(histchar as u32) && it.next().is_some() {
return Some(HighlightRole::history_expansion);
}
}
if let Ok(tab) = aliastab_lock().read() {
if let Some(node) = tab.get(clean) {
use crate::ported::zsh_h::ALIAS_GLOBAL;
if node.node.flags & ALIAS_GLOBAL as i32 != 0 {
return Some(HighlightRole::global_alias);
}
}
}
if !self.io_still_ok() {
return None; }
let expanded = fsh_expand_for_path(src)?;
let abs =
crate::zle_file_tester::path_apply_working_directory(&expanded, &self.working_directory);
match std::fs::metadata(&abs) {
Ok(md) if md.is_dir() => Some(HighlightRole::path_to_dir),
Ok(_) => Some(HighlightRole::path),
Err(_) => None,
}
}
fn visit_redirection(&mut self, op: &TokSpan, target: Option<&TokSpan>) {
self.color_span(op.start, op.end, HighlightRole::redirection);
let Some(target) = target else { return };
let target_text = target.text.clone().unwrap_or_default();
let _ = (&target_text, redir_tok_mode(op.tok, &target.clean_text()));
self.visit_argument(target, false, true, true);
}
fn visit_variable_assignment(&mut self, t: &TokSpan) {
let start = t.start;
let end = t.end.min(self.color_array.len());
if start >= end {
return;
}
let src: String = self.span_text(t);
color_string_internal(
&src,
HighlightSpec::with_fg(HighlightRole::assign),
&mut self.color_array[start..end],
);
if let Some(eq) = src.chars().position(|c| c == '=') {
let rest: String = src.chars().skip(eq + 1).collect();
if rest.starts_with("$((") {
let lo = start + eq + 1;
self.color_span(lo, lo + 1, HighlightRole::dollar_in_dquote);
self.color_span(lo + 1, lo + 3, HighlightRole::double_paren);
self.complex_brackets.push(lo + 1);
self.complex_brackets.push(lo + 2);
let mut body_end = end;
if rest.ends_with("))") && end >= lo + 5 {
body_end = end - 2;
self.color_span(body_end, end, HighlightRole::double_paren);
self.complex_brackets.push(body_end);
self.complex_brackets.push(body_end + 1);
}
self.highlight_math(lo + 3, body_end);
}
}
if let Some(offset) = src.chars().position(|c| c == '=') {
let var_name: String = src.chars().take(offset).collect();
if valid_var_name(&var_name) {
self.pending_variables.push(var_name);
}
}
}
fn highlight_for_loop(&mut self, lo: usize, hi: usize) {
let hi = hi.min(self.buff_chars.len());
if lo >= hi {
return;
}
const OPS: &str = "+<>=:*|&^~-";
let mut i = lo;
while i < hi {
let c = self.buff_chars[i];
if c.is_alphabetic() || c == '_' {
let mut j = i;
while j < hi && (self.buff_chars[j].is_alphanumeric() || self.buff_chars[j] == '_') {
j += 1;
}
self.color_span(i, j, HighlightRole::for_loop_variable);
i = j;
} else if c.is_ascii_digit() {
let mut j = i;
while j < hi && self.buff_chars[j].is_ascii_digit() {
j += 1;
}
self.color_span(i, j, HighlightRole::for_loop_number);
i = j;
} else if OPS.contains(c) {
let mut j = i;
while j < hi && OPS.contains(self.buff_chars[j]) {
j += 1;
}
self.color_span(i, j, HighlightRole::for_loop_operator);
i = j;
} else if c == ';' {
self.color_span(i, i + 1, HighlightRole::for_loop_separator);
i += 1;
} else {
i += 1;
}
}
}
fn mark_assign_parens(&mut self, t: &TokSpan, in_array_assignment: &mut bool) {
let n = self.buff_chars.len();
let (lo, hi) = (t.start.min(n), t.end.min(n));
let Some(eq) = self.buff_chars[lo..hi].iter().position(|&c| c == '=') else {
return;
};
let Some(open) = self.buff_chars[lo + eq..hi].iter().position(|&c| c == '(') else {
return;
};
let open = lo + eq + open;
if open != lo + eq + 1 {
return; }
self.color_span(open, open + 1, HighlightRole::assign_array_bracket);
self.complex_brackets.push(open);
match self.buff_chars[open..hi].iter().rposition(|&c| c == ')') {
Some(rel) => {
let close = open + rel;
self.color_span(close, close + 1, HighlightRole::assign_array_bracket);
self.complex_brackets.push(close);
}
None => *in_array_assignment = true,
}
}
fn highlight_math(&mut self, lo: usize, hi: usize) {
let hi = hi.min(self.buff_chars.len());
if lo >= hi {
return;
}
let mut i = lo;
while i < hi {
let c = self.buff_chars[i];
if c.is_ascii_digit() {
let mut j = i;
while j < hi && (self.buff_chars[j].is_ascii_digit() || self.buff_chars[j] == '.') {
j += 1;
}
self.color_span(i, j, HighlightRole::mathnum); i = j;
} else if c.is_alphabetic() || c == '_' {
let mut j = i;
while j < hi && (self.buff_chars[j].is_alphanumeric() || self.buff_chars[j] == '_') {
j += 1;
}
let name: String = self.buff_chars[i..j].iter().collect();
let role = if math_name_exists(&name) || self.pending_variables.contains(&name) {
HighlightRole::mathvar } else {
HighlightRole::matherr };
self.color_span(i, j, role);
i = j;
} else if c == '$' {
let mut j = i + 1;
let braced = self.buff_chars.get(j) == Some(&'{');
if braced {
j += 1;
}
let name_lo = j;
while j < hi && (self.buff_chars[j].is_alphanumeric() || self.buff_chars[j] == '_') {
j += 1;
}
let name: String = self.buff_chars[name_lo..j].iter().collect();
if braced && self.buff_chars.get(j) == Some(&'}') {
j += 1;
}
let role = if !name.is_empty()
&& (math_name_exists(&name) || self.pending_variables.contains(&name))
{
HighlightRole::dollar_in_dquote } else {
HighlightRole::matherr };
self.color_span(i, j, role);
i = j;
} else {
i += 1;
}
}
}
fn highlight_brackets(&mut self) {
let chars = &self.buff_chars;
let n = chars.len();
let mut pos_level: Vec<(usize, i32)> = Vec::new();
let mut stack: Vec<usize> = Vec::new();
let mut paired: Vec<bool> = Vec::new();
#[derive(PartialEq)]
enum Q {
None_,
Single,
Double,
DollarSingle,
}
let mut quoting = Q::None_;
let mut i = 0usize;
while i < n {
let c = chars[i];
if c == '\\' {
if quoting == Q::Single {
i += 1;
} else {
i += 2;
}
continue;
}
match c {
'"' if quoting != Q::Single && quoting != Q::DollarSingle => {
quoting = if quoting == Q::Double { Q::None_ } else { Q::Double };
}
'\'' if quoting != Q::Double => {
quoting = match quoting {
Q::Single | Q::DollarSingle => Q::None_,
_ if i > 0 && chars[i - 1] == '$' => Q::DollarSingle,
_ => Q::Single,
};
}
'(' | '{' | '[' if quoting == Q::None_ && !self.complex_brackets.contains(&i) => {
stack.push(i);
pos_level.push((i, stack.len() as i32));
paired.push(false);
}
')' | '}' | ']' if quoting == Q::None_ && !self.complex_brackets.contains(&i) => {
if let Some(open) = stack.pop() {
pos_level.push((i, stack.len() as i32 + 1));
paired.push(false);
let opener = chars[open];
let matches = matches!((opener, c), ('(', ')') | ('{', '}') | ('[', ']'));
if matches {
let last = paired.len() - 1;
paired[last] = true;
if let Some(k) = pos_level.iter().position(|&(p, _)| p == open) {
paired[k] = true;
}
}
} else {
pos_level.push((i, -1));
paired.push(false);
}
}
_ => (),
}
i += 1;
}
for (k, &(pos, level)) in pos_level.iter().enumerate() {
let role = if paired[k] {
match (level - 1).rem_euclid(3) + 1 {
1 => HighlightRole::bracket_level_1,
2 => HighlightRole::bracket_level_2,
_ => HighlightRole::bracket_level_3,
}
} else {
HighlightRole::error
};
self.color_span(pos, pos + 1, role);
}
}
fn color_gap_comments(&mut self, toks: &[TokSpan]) {
let len = self.buff_chars.len();
let mut gaps: Vec<(usize, usize)> = Vec::new();
let mut prev_end = 0usize;
for t in toks {
if t.start > prev_end {
gaps.push((prev_end, t.start.min(len)));
}
prev_end = prev_end.max(t.end);
}
if prev_end < len {
gaps.push((prev_end, len));
}
for (lo, hi) in gaps {
let mut j = lo;
while j < hi {
let c = self.buff_chars[j];
if c == '#' {
let eol = self.buff_chars[j..hi]
.iter()
.position(|&c| c == '\n')
.map(|p| j + p)
.unwrap_or(hi);
self.color_span(j, eol, HighlightRole::comment);
j = eol;
}
j += 1;
}
}
}
}
fn locate_cmdsubst_span(chars: &[char], from: usize) -> Option<(usize, Option<usize>)> {
let mut i = from;
let mut in_squote = false;
while i + 1 < chars.len() {
let c = chars[i];
if in_squote {
if c == '\'' {
in_squote = false;
}
} else if c == '\'' {
in_squote = true;
} else if c == '\\' {
i += 1;
} else if c == '$' && chars[i + 1] == '(' {
let mut depth = 0i32;
let mut j = i + 1;
while j < chars.len() {
match chars[j] {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some((i, Some(j)));
}
}
_ => (),
}
j += 1;
}
return Some((i, None));
}
i += 1;
}
None
}
fn contains_pending_variable(pending_variables: &[String], haystack: &str) -> bool {
let hay: Vec<char> = haystack.chars().collect();
for var_name in pending_variables {
let needle: Vec<char> = var_name.chars().collect();
if needle.is_empty() || hay.len() < needle.len() {
continue;
}
let mut nextpos = 0usize;
while nextpos + needle.len() <= hay.len() {
let Some(relpos) = hay[nextpos..]
.windows(needle.len())
.position(|w| w == needle.as_slice())
else {
break;
};
let pos = nextpos + relpos;
nextpos = pos + 1;
if pos == 0 || hay[pos - 1] != '$' {
continue; }
let end = pos + needle.len();
if end < hay.len() && valid_var_name_char(hay[end]) {
continue; }
return true;
}
}
false
}
fn redir_tok_mode(tokv: lextok, target: &str) -> RedirectionMode {
let looks_fd = target == "-" || target.chars().all(|c| c.is_ascii_digit());
match tokv {
OUTANG_TOK => {
if isset(CLOBBER) {
RedirectionMode::Overwrite
} else {
RedirectionMode::NoClob
}
}
OUTANGBANG => RedirectionMode::Overwrite,
DOUTANG | DOUTANGBANG => RedirectionMode::Append,
INANG_TOK => RedirectionMode::Input,
INOUTANG => RedirectionMode::Overwrite, INANGAMP => RedirectionMode::Fd,
OUTANGAMP | OUTANGAMPBANG => {
if looks_fd {
RedirectionMode::Fd
} else {
RedirectionMode::Overwrite }
}
DOUTANGAMP | DOUTANGAMPBANG => {
if looks_fd {
RedirectionMode::Fd
} else {
RedirectionMode::Append
}
}
crate::ported::zsh_h::AMPOUTANG => RedirectionMode::Overwrite,
_ => RedirectionMode::Input,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lock() -> std::sync::MutexGuard<'static, ()> {
crate::test_util::global_state_lock()
}
fn spans_of(line: &str) -> Vec<(String, i32)> {
lex_line_tokens(line)
.iter()
.map(|t| {
(
line.chars()
.skip(t.start)
.take(t.end - t.start)
.collect::<String>(),
t.tok,
)
})
.collect()
}
#[test]
fn lex_line_tokens_leaves_unget_buf_untouched() {
use crate::ported::lex::LEX_UNGET_BUF;
let _g = lock();
for line in [
"{ print ok }",
"( print sub )",
"function q(){ print fq }",
"echo hi",
] {
LEX_UNGET_BUF.with_borrow_mut(|b| {
b.clear();
b.push_back('X');
b.push_back('\n');
});
let _ = lex_line_tokens(line);
let after: Vec<char> = LEX_UNGET_BUF.with_borrow(|b| b.iter().copied().collect());
assert_eq!(
after,
vec!['X', '\n'],
"LEX_UNGET_BUF corrupted by highlight walk of {line:?}"
);
}
LEX_UNGET_BUF.with_borrow_mut(|b| b.clear());
}
#[test]
fn lex_spans_match_source() {
let _g = lock();
let spans = spans_of("echo hello world");
let words: Vec<&str> = spans
.iter()
.filter(|(_, t)| *t == STRING_LEX)
.map(|(s, _)| s.as_str())
.collect();
assert_eq!(words, vec!["echo", "hello", "world"], "spans {spans:?}");
}
fn roles(line: &str) -> Vec<HighlightRole> {
let ctx = OperationContext::empty();
let mut colors = Vec::new();
highlight_shell(line, &mut colors, &ctx, true, None);
colors.iter().map(|c| c.foreground).collect()
}
fn role_of(line: &str, word: &str) -> HighlightRole {
let at = line.find(word).expect("word not in line");
let at = line[..at].chars().count();
roles(line)[at]
}
#[test]
fn precommand_table_is_exact_membership() {
for yes in [
"builtin", "command", "exec", "nocorrect", "noglob", "pkexec", "sudo", "doas",
] {
assert!(fsh_is_precommand(yes), "{yes} is a precommand");
}
for no in ["", "sud", "commander", "execute", "noglobber", "Sudo", "-"] {
assert!(!fsh_is_precommand(no), "{no} is NOT a precommand");
}
}
#[test]
fn afp_option_predicate_both_directions() {
assert!(fsh_afp_option("-v", "pvV-"));
assert!(fsh_afp_option("-pv", "pvV-"));
assert!(fsh_afp_option("--", "pvV-"));
assert!(fsh_afp_option("-a", "cla-"));
assert!(!fsh_afp_option("-x", "pvV-"), "-x is not a `command` option");
assert!(!fsh_afp_option("-", "pvV-"), "a bare - has no option letters");
assert!(!fsh_afp_option("v", "pvV-"), "no leading hyphen");
assert!(!fsh_afp_option("", "pvV-"));
}
#[test]
fn globbing_ext_predicate_both_directions() {
for yes in ["(#b)a*", "(#B)x", "(#m)y", "(#c1,3)z", "a##b"] {
assert!(fsh_is_globbing_ext(yes), "{yes} carries an extended glob");
}
for no in ["", "a#b", "plain", "*.c", "a\\##b"] {
assert!(!fsh_is_globbing_ext(no), "{no} has no extended glob");
}
}
#[test]
fn printf_conversion_scanner_both_directions() {
let scan = |s: &str| {
let c: Vec<char> = s.chars().collect();
printf_conversions(&c)
};
assert_eq!(scan("'%s'"), vec![(1, 3)]);
assert_eq!(scan("\"%s=%d\\n\""), vec![(1, 3), (4, 6)]);
assert_eq!(scan("'%-10.3f'"), vec![(1, 8)]);
assert!(scan("no conversions here").is_empty());
assert!(scan("100%").is_empty(), "a trailing %% converts nothing");
assert!(scan("%z").is_empty(), "z is not a conversion letter");
assert!(scan("%").is_empty());
}
#[test]
fn reserved_words_resolve_through_the_type_ladder() {
let _g = lock();
assert_eq!(role_of("if true; then :; fi", "if"), HighlightRole::keyword);
assert_eq!(role_of("while :; do :; done", "while"), HighlightRole::keyword);
assert_eq!(
role_of("nocorrect ls", "nocorrect"),
HighlightRole::precommand
);
for decl in ["typeset", "declare", "local", "export", "readonly", "integer"] {
let line = format!("{decl} x");
assert_eq!(
role_of(&line, decl),
HighlightRole::builtin_,
"{decl} is a builtin before it is a reserved word"
);
}
}
#[test]
fn precommand_passes_command_position_along() {
let _g = lock();
assert_eq!(role_of("sudo ls", "sudo"), HighlightRole::precommand);
assert_ne!(
role_of("sudo ls", "ls"),
HighlightRole::param,
"the word after sudo is a command"
);
assert_eq!(
role_of("command zzqwx_not_a_command", "zzqwx_not_a_command"),
HighlightRole::error,
"and an invalid one still reports"
);
assert_eq!(role_of("sudo -u root ls", "-u"), HighlightRole::option);
assert_eq!(role_of("sudo -u root ls", "root"), HighlightRole::normal);
assert_ne!(role_of("sudo -u root ls", "ls"), HighlightRole::param);
}
#[test]
fn glob_word_is_whole_word_and_quoting_defeats_it() {
let _g = lock();
let r = roles("ls *.c");
assert!(
r[3..6].iter().all(|&x| x == HighlightRole::globbing),
"the whole word globs, not just the star: {r:?}"
);
let q = roles("ls '*.c'");
assert!(
!q.iter().any(|&x| x == HighlightRole::globbing),
"a quoted glob is a string: {q:?}"
);
let n = roles("noglob print *.c");
assert!(
!n.iter().any(|&x| x == HighlightRole::globbing),
"noglob suppresses glob styling: {n:?}"
);
}
#[test]
fn option_words_take_the_option_styles() {
let _g = lock();
assert_eq!(role_of("ls -l", "-l"), HighlightRole::option);
assert_eq!(role_of("ls -", "-"), HighlightRole::option);
assert_eq!(
role_of("ls --color", "--color"),
HighlightRole::double_hyphen_option
);
let after = roles("ls -- -l");
assert_eq!(
after[6],
HighlightRole::param,
"after a bare -- nothing is an option: {after:?}"
);
}
#[test]
fn brackets_are_levelled_and_unmatched_ones_report() {
let _g = lock();
let r = roles("(ls)");
assert_eq!(r[0], HighlightRole::bracket_level_1);
assert_eq!(r[3], HighlightRole::bracket_level_1);
let n = roles("print ${(U)HOME}");
assert_eq!(n[7], HighlightRole::bracket_level_1, "{{ is level 1");
assert_eq!(n[8], HighlightRole::bracket_level_2, "( inside is level 2");
assert_eq!(n[10], HighlightRole::bracket_level_2);
assert_eq!(n[15], HighlightRole::bracket_level_1);
let bad = roles("print (");
assert_eq!(
bad[6],
HighlightRole::error,
"an unmatched bracket is unknown-token: {bad:?}"
);
}
#[test]
fn every_separator_is_a_commandseparator() {
let _g = lock();
for (line, at) in [("ls && ls", 3), ("ls || ls", 3), ("ls; ls", 2), ("ls | ls", 3)] {
let r = roles(line);
assert_eq!(
r[at],
HighlightRole::statement_terminator,
"{line} at {at}: {r:?}"
);
}
}
#[test]
fn paths_are_styled_only_when_they_exist() {
let _g = lock();
assert_eq!(
role_of("cat /etc/hosts", "/etc/hosts"),
HighlightRole::path
);
assert_eq!(role_of("cat /tmp", "/tmp"), HighlightRole::path_to_dir);
assert_eq!(
role_of("cat /nonexistent_zzqwx", "/nonexistent_zzqwx"),
HighlightRole::param,
"a path that does not exist is a plain word"
);
assert_eq!(
role_of("cat /etc/host", "/etc/host"),
HighlightRole::param,
"a PREFIX of an existing path is not a path"
);
}
#[test]
fn variable_role_depends_on_quoting() {
let _g = lock();
assert_eq!(role_of("print $HOME", "$HOME"), HighlightRole::variable);
assert_eq!(
role_of("print \"a $HOME b\"", "$HOME"),
HighlightRole::dollar_in_dquote
);
}
#[test]
fn an_unterminated_quote_is_not_an_error() {
let _g = lock();
let r = roles("print 'abc");
assert_eq!(r[6], HighlightRole::quote, "{r:?}");
assert!(!r.iter().any(|&x| x == HighlightRole::error), "{r:?}");
let d = roles("print \"abc");
assert_eq!(d[6], HighlightRole::dquoted, "{d:?}");
assert!(!d.iter().any(|&x| x == HighlightRole::error), "{d:?}");
}
#[test]
fn history_expansion_needs_a_word_after_the_bang() {
let _g = lock();
assert_eq!(
role_of("print !!", "!!"),
HighlightRole::history_expansion
);
assert_eq!(
role_of("sudo !!", "!!"),
HighlightRole::history_expansion,
"also in command position"
);
assert_ne!(
role_of("! ls", "!"),
HighlightRole::history_expansion,
"a lone ! is the negation reserved word, not an expansion"
);
}
#[test]
fn command_substitution_body_uses_the_secondary_theme() {
let _g = lock();
let ctx = OperationContext::empty();
let mut colors = Vec::new();
highlight_shell("print $(ls)", &mut colors, &ctx, true, None);
assert!(colors[8].secondary, "the nested command is secondary");
assert!(!colors[6].secondary, "the leading $ is not");
let mut nested = Vec::new();
highlight_shell("print $(print $(ls))", &mut nested, &ctx, true, None);
assert!(
!nested[8].secondary,
"an outer body with a nested paren is not recursed: {:?}",
&nested[6..14]
);
assert!(nested[16].secondary, "the innermost one is");
}
#[test]
fn math_numbers_and_variables_split_on_existence() {
let _g = lock();
let r = roles("print $((1+2))");
assert_eq!(r[9], HighlightRole::mathnum, "{r:?}");
assert_eq!(r[11], HighlightRole::mathnum, "{r:?}");
assert_eq!(r[7], HighlightRole::double_paren, "{r:?}");
crate::ported::params::setsparam("zzmath_defined", "1");
assert_eq!(
role_of("print $((zzmath_defined))", "zzmath_defined"),
HighlightRole::mathvar,
"a defined parameter is a mathvar"
);
crate::ported::params::unsetparam("zzmath_defined");
assert_eq!(
role_of("print $((zzqwx_no_such_param))", "zzqwx_no_such_param"),
HighlightRole::matherr,
"an unset name is a math error"
);
}
#[test]
fn here_string_operator_and_text() {
let _g = lock();
assert_eq!(role_of("cat <<< hi", "<<<"), HighlightRole::here_string_tri);
assert_eq!(
role_of("cat <<< hi", "hi"),
HighlightRole::here_string_text
);
assert_eq!(
role_of("cat <<< $HOME", "$HOME"),
HighlightRole::here_string_var
);
}
#[test]
fn typeset_arguments_are_assignments_not_options() {
let _g = lock();
assert_eq!(role_of("typeset -g x", "-g"), HighlightRole::assign);
assert_eq!(role_of("declare -a arr", "-a"), HighlightRole::assign);
assert_eq!(
role_of("ls -g", "-g"),
HighlightRole::option,
"outside typeset the same word IS an option"
);
}
#[test]
fn autoload_chroma_splits_on_fpath() {
let _g = lock();
let dir = std::env::temp_dir().join("zshrs_hl_fpath_test");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("zzfn_present"), "zzfn_present() { : }").unwrap();
crate::ported::params::setaparam("fpath", vec![dir.to_string_lossy().into_owned()]);
assert_eq!(
role_of("autoload -Uz zzfn_present", "zzfn_present"),
HighlightRole::correct_subtle
);
assert_eq!(
role_of("autoload -Uz zzfn_absent", "zzfn_absent"),
HighlightRole::incorrect_subtle
);
assert_eq!(
role_of("autoload -Uz zzfn_present", "-Uz"),
HighlightRole::option
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn zsh_highlight_styles_overrides_the_new_defaults() {
let _g = lock();
let plain = HighlightColorResolver::resolve_spec_uncached(&HighlightSpec::with_fg(
HighlightRole::globbing,
));
crate::ported::params::sethparam(
"ZSH_HIGHLIGHT_STYLES",
vec!["globbing".into(), "fg=magenta".into()],
);
let themed = HighlightColorResolver::resolve_spec_uncached(&HighlightSpec::with_fg(
HighlightRole::globbing,
));
crate::ported::params::unsetparam("ZSH_HIGHLIGHT_STYLES");
assert_ne!(
plain, themed,
"the user's globbing style must beat the built-in fg=blue,bold"
);
}
#[test]
fn lex_spans_operators() {
let _g = lock();
let toks = lex_line_tokens("a && b || c");
let ops: Vec<i32> = toks.iter().map(|t| t.tok).collect();
assert!(ops.contains(&DAMPER), "toks {ops:?}");
assert!(ops.contains(&DBAR), "toks {ops:?}");
let damper = toks.iter().find(|t| t.tok == DAMPER).unwrap();
let src: String = "a && b || c"
.chars()
.skip(damper.start)
.take(damper.end - damper.start)
.collect();
assert_eq!(src.trim(), "&&");
}
#[test]
fn lex_spans_cmdpos_flag() {
let _g = lock();
let toks = lex_line_tokens("echo foo; ls bar");
let strings: Vec<(String, bool)> = toks
.iter()
.filter(|t| t.tok == STRING_LEX)
.map(|t| (t.clean_text(), t.cmdpos))
.collect();
assert_eq!(
strings,
vec![
("echo".to_owned(), true),
("foo".to_owned(), false),
("ls".to_owned(), true),
("bar".to_owned(), false)
]
);
}
#[test]
fn lex_tolerates_unterminated_quote() {
let _g = lock();
let toks = lex_line_tokens("echo 'in progress");
assert!(
toks.iter().filter(|t| t.tok == STRING_LEX).count() >= 2,
"unterminated quote must still lex as STRING: {:?}",
toks.iter().map(|t| t.tok).collect::<Vec<_>>()
);
}
#[test]
fn color_string_quotes_and_vars() {
let _g = lock();
let s = "a'q'\"d$V\"$X*";
let n = s.chars().count();
let mut colors = vec![HighlightSpec::default(); n];
color_string_internal(s, HighlightSpec::with_fg(HighlightRole::param), &mut colors);
let roles: Vec<HighlightRole> = colors.iter().map(|c| c.foreground).collect();
assert_eq!(roles[0], HighlightRole::param);
assert_eq!(roles[1], HighlightRole::quote);
assert_eq!(roles[2], HighlightRole::quote);
assert_eq!(roles[3], HighlightRole::quote);
assert_eq!(roles[4], HighlightRole::dquoted);
assert_eq!(roles[5], HighlightRole::dquoted);
assert_eq!(roles[6], HighlightRole::dollar_in_dquote);
assert_eq!(roles[7], HighlightRole::dollar_in_dquote);
assert_eq!(roles[8], HighlightRole::dquoted);
assert_eq!(roles[9], HighlightRole::param);
assert_eq!(roles[10], HighlightRole::param);
assert_eq!(roles[11], HighlightRole::param);
}
#[test]
fn color_string_unclosed_quote_keeps_the_quote_style() {
let _g = lock();
let s = "'unclosed";
let n = s.chars().count();
let mut colors = vec![HighlightSpec::default(); n];
color_string_internal(s, HighlightSpec::with_fg(HighlightRole::param), &mut colors);
assert!(colors.iter().all(|c| c.foreground == HighlightRole::quote));
}
#[test]
fn color_string_dollar_quote_escapes() {
let _g = lock();
let s = "$'\\x41'";
let n = s.chars().count();
let mut colors = vec![HighlightSpec::default(); n];
color_string_internal(s, HighlightSpec::with_fg(HighlightRole::param), &mut colors);
let roles: Vec<HighlightRole> = colors.iter().map(|c| c.foreground).collect();
assert_eq!(roles[2], HighlightRole::escape, "roles {roles:?}");
let s2 = "$'\\U110000'";
let n2 = s2.chars().count();
let mut colors2 = vec![HighlightSpec::default(); n2];
color_string_internal(
s2,
HighlightSpec::with_fg(HighlightRole::param),
&mut colors2,
);
assert_eq!(colors2[2].foreground, HighlightRole::escape, "{colors2:?}");
let s3 = "$'\\x'";
let n3 = s3.chars().count();
let mut colors3 = vec![HighlightSpec::default(); n3];
color_string_internal(
s3,
HighlightSpec::with_fg(HighlightRole::param),
&mut colors3,
);
assert_eq!(colors3[2].foreground, HighlightRole::error, "{colors3:?}");
}
#[test]
fn color_variable_subscript() {
let _g = lock();
let s: Vec<char> = "$arr[1]x".chars().collect();
let mut colors = vec![HighlightSpec::default(); s.len()];
let consumed = color_variable(&s, &mut colors, HighlightRole::variable);
assert_eq!(consumed, 7);
assert_eq!(colors[0].foreground, HighlightRole::variable);
assert_eq!(colors[4].foreground, HighlightRole::variable); assert_eq!(colors[6].foreground, HighlightRole::variable); }
#[test]
fn extension_builtins_are_commands_but_unknown_words_are_not() {
let _g = lock();
let ctx = OperationContext::empty();
for name in ["provenance", "dbview"] {
let line = format!("{name} -m x");
let mut colors = Vec::new();
highlight_shell(&line, &mut colors, &ctx, true, None);
assert_eq!(
colors[0].foreground,
HighlightRole::builtin_,
"{name} is an extension builtin and must colour as a builtin"
);
}
for bogus in ["provenanc", "zzqwx", "dbvie"] {
let line = format!("{bogus} arg");
let mut colors = Vec::new();
highlight_shell(&line, &mut colors, &ctx, true, None);
assert_eq!(
colors[0].foreground,
HighlightRole::error,
"{bogus} is not a command and must colour as unknown-token"
);
}
}
#[test]
fn highlight_valid_and_invalid_command() {
let _g = lock();
let ctx = OperationContext::empty();
let line = "echo hi";
let mut colors = Vec::new();
highlight_shell(line, &mut colors, &ctx, true, None);
assert_eq!(colors.len(), line.chars().count());
assert_eq!(
colors[0].foreground,
HighlightRole::builtin_,
"colors {colors:?}"
);
let line2 = "definitely_not_a_cmd_zshrs_x hi";
let mut colors2 = Vec::new();
highlight_shell(line2, &mut colors2, &ctx, true, None);
assert_eq!(colors2[0].foreground, HighlightRole::error);
let arg_pos = line2.chars().count() - 1;
assert_eq!(colors2[arg_pos].foreground, HighlightRole::param);
}
#[test]
fn highlight_reserved_word_and_separator() {
let _g = lock();
let ctx = OperationContext::empty();
let line = "if true; then echo x; fi";
let mut colors = Vec::new();
highlight_shell(line, &mut colors, &ctx, true, None);
assert_eq!(colors[0].foreground, HighlightRole::keyword, "{colors:?}");
let semi = line.chars().position(|c| c == ';').unwrap();
assert_eq!(colors[semi].foreground, HighlightRole::statement_terminator);
}
#[test]
fn highlight_assignment_is_one_flat_assign_span() {
let _g = lock();
let ctx = OperationContext::empty();
let line = "FOO=bar echo x";
let mut colors = Vec::new();
highlight_shell(line, &mut colors, &ctx, true, None);
let eq = line.chars().position(|c| c == '=').unwrap();
for i in 0..line.chars().position(|c| c == ' ').unwrap() {
assert_eq!(colors[i].foreground, HighlightRole::assign, "{colors:?}");
}
assert_eq!(colors[eq].foreground, HighlightRole::assign, "{colors:?}");
}
#[test]
fn highlight_io_off_assumes_valid() {
let _g = lock();
let ctx = OperationContext::empty();
let line = "definitely_not_a_cmd_zshrs_x";
let mut colors = Vec::new();
highlight_shell(line, &mut colors, &ctx, false, None);
assert_eq!(colors[0].foreground, HighlightRole::command);
}
#[test]
fn resolver_default_palette() {
let _g = lock();
let attr = HighlightColorResolver::resolve_spec_uncached(&HighlightSpec::with_fg(
HighlightRole::command,
));
assert_ne!(attr, 0, "command role must default to a visible style");
let err = HighlightColorResolver::resolve_spec_uncached(&HighlightSpec::with_fg(
HighlightRole::error,
));
assert_ne!(err, 0);
assert_ne!(attr, err, "command and error styles must differ");
}
#[test]
fn contains_pending_variable_matches_dollar_use() {
let vars = vec!["x".to_owned()];
assert!(contains_pending_variable(&vars, "$x"));
assert!(contains_pending_variable(&vars, "dir/$x/log"));
assert!(!contains_pending_variable(&vars, "x"));
assert!(!contains_pending_variable(&vars, "$xy"));
}
#[test]
fn locate_cmdsubst_span_finds_nested() {
let chars: Vec<char> = "a$(b $(c) d)e".chars().collect();
let (open, close) = locate_cmdsubst_span(&chars, 0).unwrap();
assert_eq!(open, 1);
assert_eq!(close, Some(11));
let chars2: Vec<char> = "a$(b".chars().collect();
let (o2, c2) = locate_cmdsubst_span(&chars2, 0).unwrap();
assert_eq!(o2, 1);
assert_eq!(c2, None);
}
}