//! Functions for syntax highlighting. use crate::abbrs::{self, with_abbrs}; use crate::ast::{ self, Argument, Ast, BlockStatement, BlockStatementHeaderVariant, DecoratedStatement, Keyword, Leaf, List, Node, NodeVisitor, Redirection, Token, Type, VariableAssignment, }; use crate::builtins::shared::builtin_exists; use crate::color::{self, RgbColor}; use crate::common::{ unescape_string_in_place, valid_var_name, valid_var_name_char, UnescapeFlags, ASCII_MAX, EXPAND_RESERVED_BASE, EXPAND_RESERVED_END, }; use crate::compat::_PC_CASE_SENSITIVE; use crate::editable_line::EditableLine; use crate::env::{EnvStackRefFFI, Environment}; use crate::expand::{ expand_one, expand_tilde, expand_to_command_and_args, ExpandFlags, ExpandResultCode, HOME_DIRECTORY, PROCESS_EXPAND_SELF_STR, }; use crate::expand::{ BRACE_BEGIN, BRACE_END, BRACE_SEP, INTERNAL_SEPARATOR, PROCESS_EXPAND_SELF, VARIABLE_EXPAND, VARIABLE_EXPAND_SINGLE, }; use crate::ffi::rgb_color_t; use crate::function; use crate::future_feature_flags::{feature_test, FeatureFlag}; use crate::history::{all_paths_are_valid, HistoryItem}; use crate::operation_context::OperationContext; use crate::output::{parse_color, Outputter}; use crate::parse_constants::{ ParseKeyword, ParseTokenType, ParseTreeFlags, SourceRange, StatementDecoration, }; use crate::parse_util::{parse_util_locate_cmdsubst_range, parse_util_slice_length}; use crate::path::{ path_apply_working_directory, path_as_implicit_cd, path_get_cdpath, path_get_path, paths_are_same_file, }; use crate::redirection::RedirectionMode; use crate::threads::assert_is_background_thread; use crate::tokenizer::{variable_assignment_equals_pos, PipeOrRedir}; use crate::wchar::{wstr, WString, L}; use crate::wchar_ext::WExt; use crate::wchar_ffi::AsWstr; use crate::wcstringutil::{ string_prefixes_string, string_prefixes_string_case_insensitive, string_suffixes_string, }; use crate::wildcard::{ANY_CHAR, ANY_STRING, ANY_STRING_RECURSIVE}; use crate::wutil::dir_iter::DirIter; use crate::wutil::fish_wcstoi; use crate::wutil::{normalize_path, waccess, wstat}; use crate::wutil::{wbasename, wdirname}; use bitflags::bitflags; use cxx::{CxxWString, SharedPtr}; use libc::{ENOENT, PATH_MAX, R_OK, W_OK}; use std::collections::hash_map::Entry; use std::collections::{HashMap, HashSet}; use std::os::fd::RawFd; use std::pin::Pin; impl HighlightSpec { pub fn new() -> Self { Self::default() } pub fn with_fg(fg: HighlightRole) -> Self { Self::with_fg_bg(fg, HighlightRole::normal) } pub fn with_fg_bg(fg: HighlightRole, bg: HighlightRole) -> Self { Self { foreground: fg, background: bg, ..Default::default() } } pub fn with_bg(bg: HighlightRole) -> Self { Self::with_fg_bg(HighlightRole::normal, bg) } } /// Given a string and list of colors of the same size, return the string with ANSI escape sequences /// representing the colors. pub fn colorize(text: &wstr, colors: &[HighlightSpec], vars: &dyn Environment) -> Vec { assert!(colors.len() == text.len()); let mut rv = HighlightColorResolver::new(); let mut outp = Outputter::new_buffering(); let mut last_color = HighlightSpec::with_fg(HighlightRole::normal); for (i, c) in text.chars().enumerate() { let color = colors[i]; if color != last_color { outp.set_color(rv.resolve_spec(&color, false, vars), RgbColor::NORMAL); last_color = color; } outp.writech(c); } outp.set_color(RgbColor::NORMAL, RgbColor::NORMAL); outp.contents().to_owned() // TODO should move } /// Perform syntax highlighting for the shell commands in buff. The result is stored in the color /// array as a color_code from the HIGHLIGHT_ enum for each character in buff. /// /// \param buffstr The buffer on which to perform syntax highlighting /// \param color The array in which to store the color codes. The first 8 bits are used for fg /// color, the next 8 bits for bg color. /// \param ctx The variables and cancellation check for this operation. /// \param io_ok If set, allow IO which may block. This means that e.g. invalid commands may be /// detected. /// \param cursor The position of the cursor in the commandline. pub fn highlight_shell( buff: &wstr, color: &mut Vec, ctx: &OperationContext<'_>, io_ok: bool, /* = false */ cursor: Option, ) { let working_directory = ctx.vars().get_pwd_slash(); let mut highlighter = Highlighter::new(buff, cursor, ctx, working_directory, io_ok); *color = highlighter.highlight(); } /// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. /// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be /// one screen redraw. #[derive(Default)] pub struct HighlightColorResolver { fg_cache: HashMap, bg_cache: HashMap, } /// highlight_color_resolver_t resolves highlight specs (like "a command") to actual RGB colors. /// It maintains a cache with no invalidation mechanism. The lifetime of these should typically be /// one screen redraw. impl HighlightColorResolver { pub fn new() -> Self { Default::default() } /// \return an RGB color for a given highlight spec. pub fn resolve_spec( &mut self, highlight: &HighlightSpec, is_background: bool, vars: &dyn Environment, ) -> RgbColor { let cache = if is_background { &mut self.bg_cache } else { &mut self.fg_cache }; match cache.entry(*highlight) { Entry::Occupied(e) => *e.get(), Entry::Vacant(e) => { let color = Self::resolve_spec_uncached(highlight, is_background, vars); e.insert(color); color } } } pub fn resolve_spec_uncached( highlight: &HighlightSpec, is_background: bool, vars: &dyn Environment, ) -> RgbColor { let mut result = RgbColor::NORMAL; let role = if is_background { highlight.background } else { highlight.foreground }; let var = vars .get_unless_empty(get_highlight_var_name(role)) .or_else(|| vars.get_unless_empty(get_highlight_var_name(get_fallback(role)))) .or_else(|| vars.get(get_highlight_var_name(HighlightRole::normal))); if let Some(var) = var { result = parse_color(&var, is_background); } // Handle modifiers. if !is_background && highlight.valid_path { if let Some(var2) = vars.get(L!("fish_color_valid_path")) { let result2 = parse_color(&var2, is_background); if result.is_normal() { result = result2; } else if !result2.is_normal() { // Valid path has an actual color, use it and merge the modifiers. let mut rescol = result2; rescol.set_bold(result.is_bold() || result2.is_bold()); rescol.set_underline(result.is_underline() || result2.is_underline()); rescol.set_italics(result.is_italics() || result2.is_italics()); rescol.set_dim(result.is_dim() || result2.is_dim()); rescol.set_reverse(result.is_reverse() || result2.is_reverse()); result = rescol; } else { if result2.is_bold() { result.set_bold(true) }; if result2.is_underline() { result.set_underline(true) }; if result2.is_italics() { result.set_italics(true) }; if result2.is_dim() { result.set_dim(true) }; if result2.is_reverse() { result.set_reverse(true) }; } } } if !is_background && highlight.force_underline { result.set_underline(true); } result } } fn command_is_valid( cmd: &wstr, decoration: StatementDecoration, working_directory: &wstr, vars: &dyn Environment, ) -> bool { // Determine which types we check, based on the decoration. let mut builtin_ok = true; let mut function_ok = true; let mut abbreviation_ok = true; let mut command_ok = true; let mut implicit_cd_ok = true; if matches!( decoration, StatementDecoration::command | StatementDecoration::exec ) { builtin_ok = false; function_ok = false; abbreviation_ok = false; command_ok = true; implicit_cd_ok = false; } else if decoration == StatementDecoration::builtin { builtin_ok = true; function_ok = false; abbreviation_ok = false; command_ok = false; implicit_cd_ok = false; } // Check them. let mut is_valid = false; // Builtins if !is_valid && builtin_ok { is_valid = builtin_exists(cmd) }; // Functions if !is_valid && function_ok { is_valid = function::exists_no_autoload(cmd) }; // Abbreviations if !is_valid && abbreviation_ok { is_valid = with_abbrs(|set| set.has_match(cmd, abbrs::Position::Command)) }; // Regular commands if !is_valid && command_ok { is_valid = path_get_path(cmd, vars).is_some() }; // Implicit cd if !is_valid && implicit_cd_ok { is_valid = path_as_implicit_cd(cmd, working_directory, vars).is_some(); } // Return what we got. return is_valid; } fn has_expand_reserved(s: &wstr) -> bool { for wc in s.chars() { if (EXPAND_RESERVED_BASE..=EXPAND_RESERVED_END).contains(&wc) { return true; } } false } // Parse a command line. Return by reference the first command, and the first argument to that // command (as a string), if any. This is used to validate autosuggestions. fn autosuggest_parse_command( buff: &wstr, ctx: &OperationContext<'_>, ) -> Option<(WString, WString)> { let ast = Ast::parse( buff, ParseTreeFlags::CONTINUE_AFTER_ERROR | ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS, None, ); // Find the first statement. let Some(jc) = ast.top().as_job_list().unwrap().get(0) else { return None; }; let Some(first_statement) = jc.job.statement.contents.as_decorated_statement() else { return None; }; if let Some(expanded_command) = statement_get_expanded_command(buff, first_statement, ctx) { let mut arg = WString::new(); // Check if the first argument or redirection is, in fact, an argument. if let Some(arg_or_redir) = first_statement.args_or_redirs.get(0) { if arg_or_redir.is_argument() { arg = arg_or_redir.argument().source(buff).to_owned(); } } return Some((expanded_command, arg)); } None } /// Given an item \p item from the history which is a proposed autosuggestion, return whether the /// autosuggestion is valid. It may not be valid if e.g. it is attempting to cd into a directory /// which does not exist. pub fn autosuggest_validate_from_history( item: &HistoryItem, working_directory: &wstr, ctx: &OperationContext<'_>, ) -> bool { assert_is_background_thread(); // Parse the string. let Some((parsed_command, mut cd_dir)) = autosuggest_parse_command(item.str(), ctx) else { // This is for autosuggestions which are not decorated commands, e.g. function declarations. return true; }; // We handle cd specially. if parsed_command == L!("cd") && !cd_dir.is_empty() { if expand_one(&mut cd_dir, ExpandFlags::SKIP_CMDSUBST, ctx, None) { if string_prefixes_string(&cd_dir, L!("--help")) || string_prefixes_string(&cd_dir, L!("-h")) { // cd --help is always valid. return true; } else { // Check the directory target, respecting CDPATH. // Permit the autosuggestion if the path is valid and not our directory. let path = path_get_cdpath(&cd_dir, working_directory, ctx.vars()); return path .map(|p| !paths_are_same_file(working_directory, &p)) .unwrap_or(false); } } } // Not handled specially. Is the command valid? let cmd_ok = builtin_exists(&parsed_command) || function::exists_no_autoload(&parsed_command) || path_get_path(&parsed_command, ctx.vars()).is_some(); if !cmd_ok { return false; } // Did the historical command have arguments that look like paths, which aren't paths now? let paths = item.get_required_paths(); if !all_paths_are_valid(paths.iter().cloned(), ctx) { return false; } true } // Highlights the variable starting with 'in', setting colors within the 'colors' array. Returns the // number of characters consumed. fn color_variable(inp: &wstr, colors: &mut [HighlightSpec]) -> usize { assert!(inp.char_at(0) == '$'); // Handle an initial run of $s. let mut idx = 0; let mut dollar_count = 0; while inp.char_at(idx) == '$' { // Our color depends on the next char. let next = inp.char_at(idx + 1); if next == '$' || valid_var_name_char(next) { colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); } else if next == '(' { colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); return idx + 1; } else { colors[idx] = HighlightSpec::with_fg(HighlightRole::error); } idx += 1; dollar_count += 1; } // Handle a sequence of variable characters. // It may contain an escaped newline - see #8444. loop { if valid_var_name_char(inp.char_at(idx)) { colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); idx += 1; } else if inp.char_at(idx) == '\\' && inp.char_at(idx + 1) == '\n' { colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); idx += 1; colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); idx += 1; } else { break; } } // Handle a slice, up to dollar_count of them. Note that we currently don't do any validation of // the slice's contents, e.g. $foo[blah] will not show an error even though it's invalid. for _slice_count in 0..dollar_count { match parse_util_slice_length(&inp[idx..]) { Some(slice_len) if slice_len > 0 => { colors[idx] = HighlightSpec::with_fg(HighlightRole::operat); colors[idx + slice_len - 1] = HighlightSpec::with_fg(HighlightRole::operat); idx += slice_len; } Some(_slice_len) => { // not a slice break; } None => { // Syntax error. Normally the entire token is colored red for us, but inside a // double-quoted string that doesn't happen. As such, color the variable + the slice // start red. Coloring any more than that looks bad, unless we're willing to try and // detect where the double-quoted string ends, and I'd rather not do that. colors[..idx + 1].fill(HighlightSpec::with_fg(HighlightRole::error)); break; } } } idx } /// This function is a disaster badly in need of refactoring. It colors an argument or command, /// without regard to command substitutions. fn color_string_internal(buffstr: &wstr, base_color: HighlightSpec, colors: &mut [HighlightSpec]) { // Clarify what we expect. assert!( [ HighlightSpec::with_fg(HighlightRole::param), HighlightSpec::with_fg(HighlightRole::option), HighlightSpec::with_fg(HighlightRole::command) ] .contains(&base_color), "Unexpected base color" ); let buff_len = buffstr.len(); colors.fill(base_color); // Hacky support for %self which must be an unquoted literal argument. if buffstr == PROCESS_EXPAND_SELF_STR { colors[..PROCESS_EXPAND_SELF_STR.len()].fill(HighlightSpec::with_fg(HighlightRole::operat)); return; } #[derive(Eq, PartialEq)] enum Mode { unquoted, single_quoted, double_quoted, } 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 = buffstr.as_char_slice()[in_pos]; match mode { Mode::unquoted => { if c == '\\' { let mut fill_color = HighlightRole::escape; // may be set to highlight_error let backslash_pos = in_pos; let mut fill_end = backslash_pos; // Move to the escaped character. in_pos += 1; let escaped_char = if in_pos < buff_len { buffstr.as_char_slice()[in_pos] } else { '\0' }; if escaped_char == '\0' { fill_end = in_pos; fill_color = HighlightRole::error; } else if matches!(escaped_char, '~' | '%') { if in_pos == 1 { fill_end = in_pos + 1; } } else if escaped_char == ',' { if bracket_count != 0 { fill_end = in_pos + 1; } } else if "abefnrtv*?$(){}[]'\"<>^ \\#;|&".contains(escaped_char) { fill_end = in_pos + 1; } else if escaped_char == 'c' { // Like \ci. So highlight three characters. fill_end = in_pos + 1; } else if "uUxX01234567".contains(escaped_char) { let mut res = 0; let mut chars = 2; let mut base = 16; let mut max_val = ASCII_MAX; match escaped_char { 'u' => { chars = 4; max_val = '\u{FFFF}'; // UCS2_MAX in_pos += 1; } 'U' => { chars = 8; // Don't exceed the largest Unicode code point - see #1107. max_val = '\u{10FFFF}'; in_pos += 1; } 'x' | 'X' => { max_val = 0xFF as char; in_pos += 1; } _ => { // a digit like \12 base = 8; chars = 3; } } // Consume for _i in 0..chars { if in_pos == buff_len { break; } let Some(d) = buffstr.as_char_slice()[in_pos].to_digit(base) else { break; }; res = (res * base) + d; in_pos += 1; } // in_pos is now at the first character that could not be converted (or // buff_len). assert!((backslash_pos..=buff_len).contains(&in_pos)); fill_end = in_pos; // It's an error if we exceeded the max value. if res > u32::from(max_val) { fill_color = HighlightRole::error; } // Subtract one from in_pos, so that the increment in the loop will move to // the next character. in_pos -= 1; } assert!(fill_end >= backslash_pos); colors[backslash_pos..fill_end].fill(HighlightSpec::with_fg(fill_color)); } else { // Not a backslash. match c { '~' => { if in_pos == 0 { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); } } '$' => { assert!(in_pos < buff_len); in_pos += color_variable(&buffstr[in_pos..], &mut colors[in_pos..]); // Subtract one to account for the upcoming loop increment. in_pos -= 1; } '?' => { if !feature_test(FeatureFlag::qmark_noglob) { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); } } '*' | '(' | ')' => { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); } '{' => { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); bracket_count += 1; } '}' => { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); bracket_count -= 1; } ',' => { if bracket_count > 0 { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::operat); } } '\'' => { 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::quote); unclosed_quote_offset = Some(in_pos); mode = Mode::double_quoted; } _ => (), // we ignore all other characters } } } // Mode 1 means single quoted string, i.e 'foo'. Mode::single_quoted => { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); if c == '\\' { // backslash if in_pos + 1 < buff_len { let escaped_char = buffstr.as_char_slice()[in_pos + 1]; if matches!(escaped_char, '\\' | '\'') { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape); // backslash colors[in_pos + 1] = HighlightSpec::with_fg(HighlightRole::escape); // escaped char in_pos += 1; // skip over backslash } } } else if c == '\'' { mode = Mode::unquoted; } } // Mode 2 means double quoted string, i.e. "foo". Mode::double_quoted => { // Slices are colored in advance, past `in_pos`, and we don't want to overwrite // that. if colors[in_pos] == base_color { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::quote); } match c { '"' => { mode = Mode::unquoted; } '\\' => { // Backslash if in_pos + 1 < buff_len { let escaped_char = buffstr.as_char_slice()[in_pos + 1]; if matches!(escaped_char, '\\' | '"' | '\n' | '$') { colors[in_pos] = HighlightSpec::with_fg(HighlightRole::escape); // backslash colors[in_pos + 1] = HighlightSpec::with_fg(HighlightRole::escape); // escaped char in_pos += 1; // skip over backslash } } } '$' => { in_pos += color_variable(&buffstr[in_pos..], &mut colors[in_pos..]); // Subtract one to account for the upcoming increment in the loop. in_pos -= 1; } _ => (), // we ignore all other characters } } } in_pos += 1; } // Error on unclosed quotes. if mode != Mode::unquoted { colors[unclosed_quote_offset.unwrap()] = HighlightSpec::with_fg(HighlightRole::error); } } /// Indicates whether the source range of the given node forms a valid path in the given /// working_directory. fn range_is_potential_path( src: &wstr, range: SourceRange, at_cursor: bool, ctx: &OperationContext, working_directory: &wstr, ) -> bool { // Skip strings exceeding PATH_MAX. See #7837. // Note some paths may exceed PATH_MAX, but this is just for highlighting. if range.length() > (PATH_MAX as usize) { return false; } // Get the node source, unescape it, and then pass it to is_potential_path along with the // working directory (as a one element list). let mut result = false; let mut token = (src[range.start()..range.end()]).to_owned(); if unescape_string_in_place( &mut token, crate::common::UnescapeStringStyle::Script(UnescapeFlags::SPECIAL), ) { // Big hack: is_potential_path expects a tilde, but unescape_string gives us HOME_DIRECTORY. // Put it back. if token.char_at(0) == HOME_DIRECTORY { token.as_char_slice_mut()[0] = '~'; } result = is_potential_path( &token, at_cursor, &[working_directory.to_owned()], ctx, PathFlags::PATH_EXPAND_TILDE, ); } result } // Tests whether the specified string cpath is the prefix of anything we could cd to. directories is // a list of possible parent directories (typically either the working directory, or the cdpath). // This does I/O! // // This is used only internally to this file, and is exposed only for testing. bitflags! { #[derive(Clone, Copy, Default)] pub struct PathFlags: u8 { // The path must be to a directory. const PATH_REQUIRE_DIR = 1 << 0; // Expand any leading tilde in the path. const PATH_EXPAND_TILDE = 1 << 1; // Normalize directories before resolving, as "cd". const PATH_FOR_CD = 1 << 2; } } /// Tests whether the specified string cpath is the prefix of anything we could cd to. directories /// is a list of possible parent directories (typically either the working directory, or the /// cdpath). This does I/O! /// /// Hack: if out_suggested_cdpath is not NULL, it returns the autosuggestion for cd. This descends /// the deepest unique directory hierarchy. /// /// We expect the path to already be unescaped. pub fn is_potential_path( potential_path_fragment: &wstr, at_cursor: bool, directories: &[WString], ctx: &OperationContext<'_>, flags: PathFlags, ) -> bool { assert_is_background_thread(); if ctx.check_cancel() { return false; } let require_dir = flags.contains(PathFlags::PATH_REQUIRE_DIR); let mut clean_potential_path_fragment = WString::new(); let mut has_magic = false; let mut path_with_magic = potential_path_fragment.to_owned(); if flags.contains(PathFlags::PATH_EXPAND_TILDE) { expand_tilde(&mut path_with_magic, ctx.vars()); } for c in path_with_magic.chars() { match c { PROCESS_EXPAND_SELF | VARIABLE_EXPAND | VARIABLE_EXPAND_SINGLE | BRACE_BEGIN | BRACE_END | BRACE_SEP | ANY_CHAR | ANY_STRING | ANY_STRING_RECURSIVE => { has_magic = true; } INTERNAL_SEPARATOR => (), _ => clean_potential_path_fragment.push(c), } } if has_magic || clean_potential_path_fragment.is_empty() { return false; } // Don't test the same path multiple times, which can happen if the path is absolute and the // CDPATH contains multiple entries. let mut checked_paths = HashSet::new(); // Keep a cache of which paths / filesystems are case sensitive. let mut case_sensitivity_cache = CaseSensitivityCache::new(); for wd in directories { if ctx.check_cancel() { return false; } let mut abs_path = path_apply_working_directory(&clean_potential_path_fragment, wd); let must_be_full_dir = abs_path.chars().last() == Some('/'); if flags.contains(PathFlags::PATH_FOR_CD) { abs_path = normalize_path(&abs_path, /*allow_leading_double_slashes=*/ true); } // Skip this if it's empty or we've already checked it. if abs_path.is_empty() || checked_paths.contains(&abs_path) { continue; } checked_paths.insert(abs_path.clone()); // If the user is still typing the argument, we want to highlight it if it's the prefix // of a valid path. This means we need to potentially walk all files in some directory. // There are two easy cases where we can skip this: // 1. If the argument ends with a slash, it must be a valid directory, no prefix. // 2. If the cursor is not at the argument, it means the user is definitely not typing it, // so we can skip the prefix-match. if must_be_full_dir || !at_cursor { if let Ok(md) = wstat(&abs_path) { if !at_cursor || md.file_type().is_dir() { return true; } } } else { // We do not end with a slash; it does not have to be a directory. let dir_name = wdirname(&abs_path); let filename_fragment = wbasename(&abs_path); if dir_name == L!("/") && filename_fragment == L!("/") { // cd ///.... No autosuggestion. return true; } if let Ok(mut dir) = DirIter::new(dir_name) { // Check if we're case insensitive. let do_case_insensitive = fs_is_case_insensitive(dir_name, dir.fd(), &mut case_sensitivity_cache); // We opened the dir_name; look for a string where the base name prefixes it. while let Some(entry) = dir.next() { let Ok(entry) = entry else { continue }; if ctx.check_cancel() { return false; } // Maybe skip directories. if require_dir && !entry.is_dir() { continue; } if string_prefixes_string(filename_fragment, &entry.name) || (do_case_insensitive && string_prefixes_string_case_insensitive( filename_fragment, &entry.name, )) { return true; } } } } } false } // Given a string, return whether it prefixes a path that we could cd into. Return that path in // out_path. Expects path to be unescaped. fn is_potential_cd_path( path: &wstr, at_cursor: bool, working_directory: &wstr, ctx: &OperationContext<'_>, flags: PathFlags, ) -> bool { let mut directories = vec![]; if string_prefixes_string(L!("./"), path) { // Ignore the CDPATH in this case; just use the working directory. directories.push(working_directory.to_owned()); } else { // Get the CDPATH. let cdpath = ctx.vars().get_unless_empty(L!("CDPATH")); let mut pathsv = match cdpath { None => vec![L!(".").to_owned()], Some(cdpath) => cdpath.as_list().to_vec(), }; // The current $PWD is always valid. pathsv.push(L!(".").to_owned()); for mut next_path in pathsv { if next_path.is_empty() { next_path = L!(".").to_owned(); } // Ensure that we use the working directory for relative cdpaths like ".". directories.push(path_apply_working_directory(&next_path, working_directory)); } } // Call is_potential_path with all of these directories. is_potential_path( path, at_cursor, &directories, ctx, flags | PathFlags::PATH_REQUIRE_DIR | PathFlags::PATH_FOR_CD, ) } pub type ColorArray = Vec; /// Syntax highlighter helper. struct Highlighter<'s> { // The string we're highlighting. Note this is a reference member variable (to avoid copying)! buff: &'s wstr, // The position of the cursor within the string. cursor: Option, // The operation context. ctx: &'s OperationContext<'s>, // Whether it's OK to do I/O. io_ok: bool, // Working directory. working_directory: WString, // The resulting colors. color_array: ColorArray, // A stack of variables that the current commandline probably defines. We mark redirections // as valid if they use one of these variables, to avoid marking valid targets as error. // TODO This should be &'s wstr pending_variables: Vec, done: bool, } impl<'s> Highlighter<'s> { pub fn new( buff: &'s wstr, cursor: Option, ctx: &'s OperationContext<'s>, working_directory: WString, can_do_io: bool, ) -> Self { Self { buff, cursor, ctx, io_ok: can_do_io, working_directory, color_array: vec![], pending_variables: vec![], done: false, } } pub fn highlight(&mut self) -> ColorArray { assert!(!self.done); self.done = true; // If we are doing I/O, we must be in a background thread. if self.io_ok { assert_is_background_thread(); } self.color_array .resize(self.buff.len(), HighlightSpec::default()); // Flags we use for AST parsing. let ast_flags = ParseTreeFlags::CONTINUE_AFTER_ERROR | ParseTreeFlags::INCLUDE_COMMENTS | ParseTreeFlags::ACCEPT_INCOMPLETE_TOKENS | ParseTreeFlags::LEAVE_UNTERMINATED | ParseTreeFlags::SHOW_EXTRA_SEMIS; let ast = Ast::parse(self.buff, ast_flags, None); self.visit_children(ast.top()); if self.ctx.check_cancel() { return std::mem::take(&mut self.color_array); } // Color every comment. let extras = &ast.extras; for range in &extras.comments { self.color_range(*range, HighlightSpec::with_fg(HighlightRole::comment)); } // Color every extra semi. for range in &extras.semis { self.color_range( *range, HighlightSpec::with_fg(HighlightRole::statement_terminator), ); } // Color every error range. for range in &extras.errors { self.color_range(*range, HighlightSpec::with_fg(HighlightRole::error)); } std::mem::take(&mut self.color_array) } /// \return a substring of our buffer. pub fn get_source(&self, r: SourceRange) -> &'s wstr { assert!(r.end() >= r.start(), "Overflow"); assert!(r.end() <= self.buff.len(), "Out of range"); &self.buff[r.start()..r.end()] } fn io_still_ok(&self) -> bool { self.io_ok && !self.ctx.check_cancel() } // Color a command. fn color_command(&mut self, node: &ast::String_) { let source_range = node.source_range(); let cmd_str = self.get_source(source_range); let arg_start = source_range.start(); color_string_internal( cmd_str, HighlightSpec::with_fg(HighlightRole::command), &mut self.color_array[arg_start..], ); } // Color a node as if it were an argument. fn color_as_argument(&mut self, node: &dyn ast::Node, options_allowed: bool /* = true */) { // node does not necessarily have type symbol_argument here. let source_range = node.source_range(); let arg_str = self.get_source(source_range); let arg_start = source_range.start(); // Color this argument without concern for command substitutions. if options_allowed && arg_str.char_at(0) == '-' { color_string_internal( arg_str, HighlightSpec::with_fg(HighlightRole::option), &mut self.color_array[arg_start..], ); } else { color_string_internal( arg_str, HighlightSpec::with_fg(HighlightRole::param), &mut self.color_array[arg_start..], ); } // Now do command substitutions. let mut cmdsub_cursor = 0; let mut cmdsub_start = 0; let mut cmdsub_end = 0; let mut cmdsub_contents = L!(""); let mut is_quoted = false; while parse_util_locate_cmdsubst_range( arg_str, &mut cmdsub_cursor, Some(&mut cmdsub_contents), &mut cmdsub_start, &mut cmdsub_end, /*accept_incomplete=*/ true, Some(&mut is_quoted), None, ) > 0 { // The cmdsub_start is the open paren. cmdsub_end is either the close paren or the end of // the string. cmdsub_contents extends from one past cmdsub_start to cmdsub_end. assert!(cmdsub_end > cmdsub_start); assert!(cmdsub_end - cmdsub_start - 1 == cmdsub_contents.len()); // Found a command substitution. Compute the position of the start and end of the cmdsub // contents, within our overall src. let arg_subcmd_start = arg_start + cmdsub_start; let arg_subcmd_end = arg_start + cmdsub_end; // Highlight the parens. The open paren must exist; the closed paren may not if it was // incomplete. assert!(cmdsub_start < arg_str.len()); self.color_array[arg_subcmd_start] = HighlightSpec::with_fg(HighlightRole::operat); if arg_subcmd_end < self.buff.len() { self.color_array[arg_subcmd_end] = HighlightSpec::with_fg(HighlightRole::operat); } // Highlight it recursively. let arg_cursor = self.cursor.map(|c| c.wrapping_sub(arg_subcmd_start)); let mut cmdsub_highlighter = Highlighter::new( cmdsub_contents, arg_cursor, self.ctx, self.working_directory.clone(), self.io_still_ok(), ); let subcolors = cmdsub_highlighter.highlight(); // Copy out the subcolors back into our array. assert!(subcolors.len() == cmdsub_contents.len()); self.color_array[arg_subcmd_start + 1..arg_subcmd_end].copy_from_slice(&subcolors); } } // Colors the source range of a node with a given color. fn color_node(&mut self, node: &dyn ast::Node, color: HighlightSpec) { self.color_range(node.source_range(), color) } // Colors a range with a given color. fn color_range(&mut self, range: SourceRange, color: HighlightSpec) { assert!(range.end() <= self.color_array.len(), "Range out of bounds"); self.color_array[range.start()..range.end()].fill(color); } // Visit the children of a node. fn visit_children(&mut self, node: &dyn Node) { node.accept(self, false); } // AST visitor implementations. fn visit_keyword(&mut self, node: &dyn Keyword) { let mut role = HighlightRole::normal; match node.keyword() { ParseKeyword::kw_begin | ParseKeyword::kw_builtin | ParseKeyword::kw_case | ParseKeyword::kw_command | ParseKeyword::kw_else | ParseKeyword::kw_end | ParseKeyword::kw_exec | ParseKeyword::kw_for | ParseKeyword::kw_function | ParseKeyword::kw_if | ParseKeyword::kw_in | ParseKeyword::kw_switch | ParseKeyword::kw_while => role = HighlightRole::keyword, ParseKeyword::kw_and | ParseKeyword::kw_or | ParseKeyword::kw_not | ParseKeyword::kw_exclam | ParseKeyword::kw_time => role = HighlightRole::operat, ParseKeyword::none => (), _ => panic!(), }; self.color_node(node.leaf_as_node(), HighlightSpec::with_fg(role)); } fn visit_token(&mut self, tok: &dyn Token) { let mut role = HighlightRole::normal; match tok.token_type() { ParseTokenType::end | ParseTokenType::pipe | ParseTokenType::background => { role = HighlightRole::statement_terminator } ParseTokenType::andand | ParseTokenType::oror => role = HighlightRole::operat, ParseTokenType::string => { // Assume all strings are params. This handles e.g. the variables a for header or // function header. Other strings (like arguments to commands) need more complex // handling, which occurs in their respective overrides of visit(). role = HighlightRole::param; } _ => (), } self.color_node(tok.leaf_as_node(), HighlightSpec::with_fg(role)); } // Visit an argument, perhaps knowing that our command is cd. fn visit_argument(&mut self, arg: &Argument, cmd_is_cd: bool, options_allowed: bool) { self.color_as_argument(arg.as_node(), options_allowed); if !self.io_still_ok() { return; } // Underline every valid path. let mut is_valid_path = false; let at_cursor = self .cursor .map_or(false, |c| arg.source_range().contains_inclusive(c)); if cmd_is_cd { // Mark this as an error if it's not 'help' and not a valid cd path. let mut param = arg.source(self.buff).to_owned(); if expand_one(&mut param, ExpandFlags::SKIP_CMDSUBST, self.ctx, None) { let is_help = string_prefixes_string(¶m, L!("--help")) || string_prefixes_string(¶m, L!("-h")); if !is_help { is_valid_path = is_potential_cd_path( ¶m, at_cursor, &self.working_directory, self.ctx, PathFlags::PATH_EXPAND_TILDE, ); if !is_valid_path { self.color_node( arg.as_node(), HighlightSpec::with_fg(HighlightRole::error), ); } } } } else if range_is_potential_path( self.buff, arg.range().unwrap(), at_cursor, self.ctx, &self.working_directory, ) { is_valid_path = true; } if is_valid_path { for i in arg.range().unwrap().start()..arg.range().unwrap().end() { self.color_array[i].valid_path = true; } } } fn visit_redirection(&mut self, redir: &Redirection) { // like 2> let oper = PipeOrRedir::try_from(redir.oper.source(self.buff)) .expect("Should have successfully parsed a pipe_or_redir_t since it was in our ast"); let mut target = redir.target.source(self.buff).to_owned(); // like &1 or file path // Color the > part. // It may have parsed successfully yet still be invalid (e.g. 9999999999999>&1) // If so, color the whole thing invalid and stop. if !oper.is_valid() { self.color_node( redir.as_node(), HighlightSpec::with_fg(HighlightRole::error), ); return; } // Color the operator part like 2>. self.color_node( &redir.oper, HighlightSpec::with_fg(HighlightRole::redirection), ); // Color the target part. // Check if the argument contains a command substitution. If so, highlight it as a param // even though it's a command redirection, and don't try to do any other validation. if has_cmdsub(&target) { self.color_as_argument(redir.target.leaf_as_node(), true); } else { // No command substitution, so we can highlight the target file or fd. For example, // disallow redirections into a non-existent directory. let target_is_valid; if !self.io_still_ok() { // I/O is disallowed, so we don't have much hope of catching anything but gross // errors. Assume it's valid. target_is_valid = true; } else if contains_pending_variable(&self.pending_variables, &target) { target_is_valid = true; } else if !expand_one(&mut target, ExpandFlags::SKIP_CMDSUBST, self.ctx, None) { // Could not be expanded. target_is_valid = false; } else { // Ok, we successfully expanded our target. Now verify that it works with this // redirection. We will probably need it as a path (but not in the case of fd // redirections). Note that the target is now unescaped. let target_path = path_apply_working_directory(&target, &self.working_directory); match oper.mode { RedirectionMode::fd => { if target == L!("-") { target_is_valid = true; } else { target_is_valid = match fish_wcstoi(&target) { Ok(fd) => fd >= 0, Err(_) => false, }; } } RedirectionMode::input => { // Input redirections must have a readable non-directory. target_is_valid = waccess(&target_path, R_OK) == 0 && match wstat(&target_path) { Ok(md) => !md.file_type().is_dir(), Err(_) => false, }; } RedirectionMode::overwrite | RedirectionMode::append | RedirectionMode::noclob => { // Test whether the file exists, and whether it's writable (possibly after // creating it). access() returns failure if the file does not exist. let file_exists; let file_is_writable; if string_suffixes_string(L!("/"), &target) { // Redirections to things that are directories is definitely not // allowed. file_exists = false; file_is_writable = false; } else { match wstat(&target_path) { Ok(md) => { // No err. We can write to it if it's not a directory and we have // permission. file_exists = true; file_is_writable = !md.file_type().is_dir() && waccess(&target_path, W_OK) == 0; } Err(err) => { if err.raw_os_error() == Some(ENOENT) { // File does not exist. Check if its parent directory is writable. let mut parent = wdirname(&target_path).to_owned(); // Ensure that the parent ends with the path separator. This will ensure // that we get an error if the parent directory is not really a // directory. if !string_suffixes_string(L!("/"), &parent) { parent.push('/'); } // Now the file is considered writable if the parent directory is // writable. file_exists = false; file_is_writable = waccess(&parent, W_OK) == 0; } else { // Other errors we treat as not writable. This includes things like // ENOTDIR. file_exists = false; file_is_writable = false; } } } } // NOCLOB means that we must not overwrite files that exist. target_is_valid = file_is_writable && !(file_exists && oper.mode == RedirectionMode::noclob); } _ => panic!(), } } self.color_node( redir.target.leaf_as_node(), HighlightSpec::with_fg(if target_is_valid { HighlightRole::redirection } else { HighlightRole::error }), ); } } fn visit_variable_assignment(&mut self, varas: &VariableAssignment) { self.color_as_argument(varas, true); // Highlight the '=' in variable assignments as an operator. if let Some(offset) = variable_assignment_equals_pos(varas.source(self.buff)) { let equals_loc = varas.source_range().start() + offset; self.color_array[equals_loc] = HighlightSpec::with_fg(HighlightRole::operat); let var_name = &varas.source(self.buff)[..offset]; self.pending_variables.push(var_name.to_owned()); } } fn visit_semi_nl(&mut self, node: &dyn Node) { self.color_node( node, HighlightSpec::with_fg(HighlightRole::statement_terminator), ) } fn visit_decorated_statement(&mut self, stmt: &DecoratedStatement) { // Color any decoration. if let Some(decoration) = stmt.opt_decoration.as_ref() { self.visit_keyword(decoration); } // Color the command's source code. // If we get no source back, there's nothing to color. if stmt.command.try_source_range().is_none() { return; } let cmd = stmt.command.source(self.buff); let mut expanded_cmd = WString::new(); let mut is_valid_cmd = false; if !self.io_still_ok() { // We cannot check if the command is invalid, so just assume it's valid. is_valid_cmd = true; } else if variable_assignment_equals_pos(cmd).is_some() { is_valid_cmd = true; } else { // Check to see if the command is valid. // Try expanding it. If we cannot, it's an error. if let Some(expanded) = statement_get_expanded_command(self.buff, stmt, self.ctx) { expanded_cmd = expanded; if !has_expand_reserved(&expanded_cmd) { is_valid_cmd = command_is_valid( &expanded_cmd, stmt.decoration(), &self.working_directory, self.ctx.vars(), ); } } } // Color our statement. if is_valid_cmd { self.color_command(&stmt.command); } else { self.color_node(&stmt.command, HighlightSpec::with_fg(HighlightRole::error)) } // Color arguments and redirections. // Except if our command is 'cd' we have special logic for how arguments are colored. let is_cd = expanded_cmd == L!("cd"); let mut is_set = expanded_cmd == L!("set"); // If we have seen a "--" argument, color all options from then on as normal arguments. let mut have_dashdash = false; for v in &stmt.args_or_redirs { if v.is_argument() { if is_set { let arg = v.argument().source(self.buff); if valid_var_name(arg) { self.pending_variables.push(arg.to_owned()); is_set = false; } } self.visit_argument(v.argument(), is_cd, !have_dashdash); if v.argument().source(self.buff) == L!("--") { have_dashdash = true; } } else { self.visit_redirection(v.redirection()); } } } fn visit_block_statement(&mut self, block: &BlockStatement) { match &*block.header { BlockStatementHeaderVariant::None => panic!(), BlockStatementHeaderVariant::ForHeader(node) => self.visit(node), BlockStatementHeaderVariant::WhileHeader(node) => self.visit(node), BlockStatementHeaderVariant::FunctionHeader(node) => self.visit(node), BlockStatementHeaderVariant::BeginHeader(node) => self.visit(node), } self.visit(&block.args_or_redirs); let pending_variables_count = self.pending_variables.len(); if let Some(fh) = block.header.as_for_header() { let var_name = fh.var_name.source(self.buff); self.pending_variables.push(var_name.to_owned()); } self.visit(&block.jobs); self.visit(&block.end); self.pending_variables.truncate(pending_variables_count); } } /// \return whether a string contains a command substitution. fn has_cmdsub(src: &wstr) -> bool { let mut cursor = 0; let mut start = 0; let mut end = 0; parse_util_locate_cmdsubst_range( src, &mut cursor, None, &mut start, &mut end, true, None, None, ) != 0 } fn contains_pending_variable(pending_variables: &[WString], haystack: &wstr) -> bool { for var_name in pending_variables { let mut nextpos = 0; while let Some(relpos) = &haystack[nextpos..] .as_char_slice() .windows(var_name.len()) .position(|w| w == var_name.as_char_slice()) { let pos = nextpos + relpos; nextpos = pos + 1; if pos == 0 || haystack.as_char_slice()[pos - 1] != '$' { continue; } let end = pos + var_name.len(); if end < haystack.len() && valid_var_name_char(haystack.as_char_slice()[end]) { continue; } return true; } } false } impl<'s, 'a> NodeVisitor<'a> for Highlighter<'s> { fn visit(&mut self, node: &'a dyn Node) { if let Some(keyword) = node.as_keyword() { return self.visit_keyword(keyword); } if let Some(token) = node.as_token() { if token.token_type() == ParseTokenType::end { self.visit_semi_nl(node); return; } self.visit_token(token); return; } match node.typ() { Type::argument => self.visit_argument(node.as_argument().unwrap(), false, true), Type::redirection => self.visit_redirection(node.as_redirection().unwrap()), Type::variable_assignment => { self.visit_variable_assignment(node.as_variable_assignment().unwrap()) } Type::decorated_statement => { self.visit_decorated_statement(node.as_decorated_statement().unwrap()) } Type::block_statement => self.visit_block_statement(node.as_block_statement().unwrap()), // Default implementation is to just visit children. _ => self.visit_children(node), } } } // Given a plain statement node in a parse tree, get the command and return it, expanded // appropriately for commands. If we succeed, return true. fn statement_get_expanded_command( src: &wstr, stmt: &ast::DecoratedStatement, ctx: &OperationContext<'_>, ) -> Option { // Get the command. Try expanding it. If we cannot, it's an error. let Some(cmd) = stmt.command.try_source(src) else { return None; }; let mut out_cmd = WString::new(); let err = expand_to_command_and_args(cmd, ctx, &mut out_cmd, None, None, false); (err == ExpandResultCode::ok).then_some(out_cmd) } fn get_highlight_var_name(role: HighlightRole) -> &'static wstr { match role { HighlightRole::normal => L!("fish_color_normal"), HighlightRole::error => L!("fish_color_error"), HighlightRole::command => L!("fish_color_command"), HighlightRole::keyword => L!("fish_color_keyword"), HighlightRole::statement_terminator => L!("fish_color_end"), HighlightRole::param => L!("fish_color_param"), HighlightRole::option => L!("fish_color_option"), HighlightRole::comment => L!("fish_color_comment"), HighlightRole::search_match => L!("fish_color_search_match"), HighlightRole::operat => L!("fish_color_operator"), HighlightRole::escape => L!("fish_color_escape"), HighlightRole::quote => L!("fish_color_quote"), HighlightRole::redirection => L!("fish_color_redirection"), HighlightRole::autosuggestion => L!("fish_color_autosuggestion"), HighlightRole::selection => L!("fish_color_selection"), HighlightRole::pager_progress => L!("fish_pager_color_progress"), HighlightRole::pager_background => L!("fish_pager_color_background"), HighlightRole::pager_prefix => L!("fish_pager_color_prefix"), HighlightRole::pager_completion => L!("fish_pager_color_completion"), HighlightRole::pager_description => L!("fish_pager_color_description"), HighlightRole::pager_secondary_background => L!("fish_pager_color_secondary_background"), HighlightRole::pager_secondary_prefix => L!("fish_pager_color_secondary_prefix"), HighlightRole::pager_secondary_completion => L!("fish_pager_color_secondary_completion"), HighlightRole::pager_secondary_description => L!("fish_pager_color_secondary_description"), HighlightRole::pager_selected_background => L!("fish_pager_color_selected_background"), HighlightRole::pager_selected_prefix => L!("fish_pager_color_selected_prefix"), HighlightRole::pager_selected_completion => L!("fish_pager_color_selected_completion"), HighlightRole::pager_selected_description => L!("fish_pager_color_selected_description"), _ => unreachable!(), } } // Table used to fetch fallback highlights in case the specified one // wasn't set. 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::command, HighlightRole::option => HighlightRole::param, 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, _ => unreachable!(), } } /// Determine if the filesystem containing the given fd is case insensitive for lookups regardless /// of whether it preserves the case when saving a pathname. /// /// Returns: /// false: the filesystem is not case insensitive /// true: the file system is case insensitive pub type CaseSensitivityCache = HashMap; fn fs_is_case_insensitive( path: &wstr, fd: RawFd, case_sensitivity_cache: &mut CaseSensitivityCache, ) -> bool { let mut result = false; if *_PC_CASE_SENSITIVE != 0 { // Try the cache first. match case_sensitivity_cache.entry(path.to_owned()) { Entry::Occupied(e) => { /* Use the cached value */ result = *e.get(); } Entry::Vacant(e) => { // Ask the system. A -1 value means error (so assume case sensitive), a 1 value means case // sensitive, and a 0 value means case insensitive. let ret = unsafe { libc::fpathconf(fd, *_PC_CASE_SENSITIVE) }; result = ret == 0; e.insert(result); } } } result } pub use highlight_ffi::{HighlightRole, HighlightSpec}; impl Default for HighlightRole { fn default() -> Self { Self::normal } } impl Default for HighlightSpec { fn default() -> Self { Self { foreground: Default::default(), background: Default::default(), valid_path: Default::default(), force_underline: Default::default(), } } } #[cxx::bridge] mod highlight_ffi { /// Describes the role of a span of text. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] #[repr(u8)] pub enum HighlightRole { normal, // normal text error, // error command, // command keyword, statement_terminator, // process separator param, // command parameter (argument) option, // argument starting with "-", up to a "--" comment, // comment search_match, // search match operat, // operator escape, // escape sequences quote, // quoted string redirection, // redirection autosuggestion, // autosuggestion selection, // Pager support. // NOTE: pager.cpp relies on these being in this order. 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, } /// Simply value type describing how a character should be highlighted.. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct HighlightSpec { pub foreground: HighlightRole, pub background: HighlightRole, pub valid_path: bool, pub force_underline: bool, } extern "Rust" { #[cxx_name = "clone"] fn clone_ffi(self: &HighlightSpec) -> Box; fn new_highlight_spec() -> Box; fn editable_line_colors(editable_line: &EditableLine) -> &[HighlightSpec]; } extern "C++" { include!("highlight.h"); include!("history.h"); include!("color.h"); include!("operation_context.h"); include!("editable_line.h"); type HistoryItem = crate::history::HistoryItem; type OperationContext<'a> = crate::operation_context::OperationContext<'a>; type rgb_color_t = crate::ffi::rgb_color_t; #[cxx_name = "EnvDyn"] type EnvDynFFI = crate::env::EnvDynFFI; #[cxx_name = "EnvStackRef"] type EnvStackRefFFI = crate::env::EnvStackRefFFI; type EditableLine = crate::editable_line::EditableLine; } extern "Rust" { #[cxx_name = "autosuggest_validate_from_history"] fn autosuggest_validate_from_history_ffi( item: &HistoryItem, working_directory: &CxxWString, ctx: &OperationContext<'static>, ) -> bool; } extern "Rust" { type HighlightColorResolver; fn new_highlight_color_resolver() -> Box; #[cxx_name = "resolve_spec"] fn resolve_spec_ffi( &mut self, highlight: &HighlightSpec, is_background: bool, vars: &EnvStackRefFFI, out: Pin<&mut rgb_color_t>, ); } extern "Rust" { type HighlightSpecListFFI; fn new_highlight_spec_list() -> Box; fn highlight_shell_ffi( bff: &CxxWString, ctx: &OperationContext<'_>, io_ok: bool, cursor: SharedPtr, ) -> Box; fn size(&self) -> usize; fn at(&self, index: usize) -> &HighlightSpec; #[cxx_name = "colorize"] fn colorize_ffi( text: &CxxWString, colors: &HighlightSpecListFFI, vars: &EnvStackRefFFI, ) -> Vec; fn push(&mut self, highlight: &HighlightSpec); } } fn colorize_ffi( text: &CxxWString, colors: &HighlightSpecListFFI, vars: &EnvStackRefFFI, ) -> Vec { colorize(text.as_wstr(), &colors.0, &*vars.0) } #[derive(Default)] pub struct HighlightSpecListFFI(pub Vec); unsafe impl cxx::ExternType for HighlightSpecListFFI { type Id = cxx::type_id!("HighlightSpecListFFI"); type Kind = cxx::kind::Opaque; } fn new_highlight_spec_list() -> Box { Box::default() } impl HighlightSpecListFFI { fn size(&self) -> usize { self.0.len() } fn at(&self, index: usize) -> &HighlightSpec { &self.0[index] } fn push(&mut self, highlight: &HighlightSpec) { self.0.push(*highlight) } } fn highlight_shell_ffi( buff: &CxxWString, ctx: &OperationContext<'_>, io_ok: bool, cursor: SharedPtr, ) -> Box { let cursor = cursor.as_ref().cloned(); let mut color = vec![]; highlight_shell(buff.as_wstr(), &mut color, ctx, io_ok, cursor); Box::new(HighlightSpecListFFI(color)) } impl HighlightColorResolver { fn resolve_spec_ffi( &mut self, highlight: &HighlightSpec, is_background: bool, vars: &EnvStackRefFFI, mut out: Pin<&mut rgb_color_t>, ) { let color = self.resolve_spec(highlight, is_background, &*vars.0); match color.typ { color::Type::None => (), color::Type::Named { idx } => { out.as_mut().set_is_named(); out.as_mut().set_name_idx(idx); } color::Type::Rgb(color) => { out.as_mut().set_is_rgb(); out.as_mut().set_color(color.r, color.g, color.b); } color::Type::Normal => out.as_mut().set_is_normal(), color::Type::Reset => out.as_mut().set_is_reset(), } if color.flags.bold { out.as_mut().set_bold(true); } if color.flags.underline { out.as_mut().set_underline(true); } if color.flags.italics { out.as_mut().set_italics(true); } if color.flags.dim { out.as_mut().set_dim(true); } if color.flags.reverse { out.as_mut().set_reverse(true); } } } fn autosuggest_validate_from_history_ffi( item: &HistoryItem, working_directory: &CxxWString, ctx: &OperationContext<'static>, ) -> bool { autosuggest_validate_from_history(item, working_directory.as_wstr(), ctx) } fn new_highlight_color_resolver() -> Box { Box::new(HighlightColorResolver::new()) } impl HighlightSpec { fn clone_ffi(&self) -> Box { Box::new(*self) } } fn new_highlight_spec() -> Box { Box::default() } unsafe impl cxx::ExternType for EditableLine { type Id = cxx::type_id!("EditableLine"); type Kind = cxx::kind::Opaque; } fn editable_line_colors(editable_line: &EditableLine) -> &[HighlightSpec] { editable_line.colors() }