fish-shell/src/tests/highlight.rs

640 lines
20 KiB
Rust

use crate::common::ScopeGuard;
use crate::env::EnvMode;
use crate::future_feature_flags::{self, FeatureFlag};
use crate::tests::prelude::*;
use crate::wchar::prelude::*;
use crate::{
env::EnvStack,
highlight::{highlight_shell, is_potential_path, HighlightRole, HighlightSpec, PathFlags},
operation_context::{OperationContext, EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT},
};
use libc::PATH_MAX;
// Helper to return a string whose length greatly exceeds PATH_MAX.
fn get_overlong_path() -> String {
let path_max = usize::try_from(PATH_MAX).unwrap();
let mut longpath = String::with_capacity(path_max * 2 + 10);
while longpath.len() <= path_max * 2 {
longpath += "/overlong";
}
longpath
}
#[test]
#[serial]
fn test_is_potential_path() {
let _cleanup = test_init();
// Directories
std::fs::create_dir_all("test/is_potential_path_test/alpha/").unwrap();
std::fs::create_dir_all("test/is_potential_path_test/beta/").unwrap();
// Files
std::fs::write("test/is_potential_path_test/aardvark", []).unwrap();
std::fs::write("test/is_potential_path_test/gamma", []).unwrap();
let wd = L!("test/is_potential_path_test/").to_owned();
let wds = [L!(".").to_owned(), wd];
let vars = EnvStack::new();
let ctx = OperationContext::background(&vars, EXPANSION_LIMIT_DEFAULT);
assert!(is_potential_path(
L!("al"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(is_potential_path(
L!("alpha/"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(is_potential_path(
L!("aard"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(!is_potential_path(
L!("aard"),
false,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(!is_potential_path(
L!("alp/"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR | PathFlags::PATH_FOR_CD
));
assert!(!is_potential_path(
L!("balpha/"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(!is_potential_path(
L!("aard"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(!is_potential_path(
L!("aarde"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(!is_potential_path(
L!("aarde"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(is_potential_path(
L!("test/is_potential_path_test/aardvark"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(is_potential_path(
L!("test/is_potential_path_test/al"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(is_potential_path(
L!("test/is_potential_path_test/aardv"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(!is_potential_path(
L!("test/is_potential_path_test/aardvark"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
assert!(!is_potential_path(
L!("test/is_potential_path_test/al/"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(!is_potential_path(
L!("test/is_potential_path_test/ar"),
true,
&wds[..],
&ctx,
PathFlags::empty()
));
assert!(is_potential_path(
L!("/usr"),
true,
&wds[..],
&ctx,
PathFlags::PATH_REQUIRE_DIR
));
}
#[test]
#[serial]
fn test_highlighting() {
let _cleanup = test_init();
let parser = TestParser::new();
// Testing syntax highlighting
parser.pushd("test/fish_highlight_test/");
let _popd = ScopeGuard::new((), |_| parser.popd());
std::fs::create_dir_all("dir").unwrap();
std::fs::create_dir_all("cdpath-entry/dir-in-cdpath").unwrap();
std::fs::write("foo", []).unwrap();
std::fs::write("bar", []).unwrap();
// Here are the components of our source and the colors we expect those to be.
#[derive(Debug)]
struct HighlightComponent<'a> {
text: &'a str,
color: HighlightSpec,
nospace: bool,
}
macro_rules! component {
( ( $text:expr, $color:expr) ) => {
HighlightComponent {
text: $text,
color: $color,
nospace: false,
}
};
( ( $text:literal, $color:expr, ns ) ) => {
HighlightComponent {
text: $text,
color: $color,
nospace: true,
}
};
}
macro_rules! validate {
( $($comp:tt),* $(,)? ) => {
let components = [
$(
component!($comp),
)*
];
let vars = parser.vars();
// Generate the text.
let mut text = WString::new();
let mut expected_colors = vec![];
for comp in &components {
if !text.is_empty() && !comp.nospace {
text.push(' ');
expected_colors.push(HighlightSpec::new());
}
text.push_str(comp.text);
expected_colors.resize(text.len(), comp.color);
}
assert_eq!(text.len(), expected_colors.len());
let mut colors = vec![];
highlight_shell(
&text,
&mut colors,
&OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND),
true, /* io_ok */
Some(text.len()),
);
assert_eq!(colors.len(), expected_colors.len());
for (i, c) in text.chars().enumerate() {
// Hackish space handling. We don't care about the colors in spaces.
if c == ' ' {
continue;
}
assert_eq!(colors[i], expected_colors[i], "Failed at position {i}, char {c}");
}
};
}
let mut param_valid_path = HighlightSpec::with_fg(HighlightRole::param);
param_valid_path.valid_path = true;
let saved_flag = future_feature_flags::test(FeatureFlag::ampersand_nobg_in_token);
future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, true);
let _restore_saved_flag = ScopeGuard::new((), |_| {
future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, saved_flag);
});
let fg = HighlightSpec::with_fg;
// Verify variables and wildcards in commands using /bin/cat.
let vars = parser.vars();
vars.set_one(
L!("CDPATH"),
EnvMode::LOCAL,
L!("./cdpath-entry").to_owned(),
);
vars.set_one(
L!("VARIABLE_IN_COMMAND"),
EnvMode::LOCAL,
L!("a").to_owned(),
);
vars.set_one(
L!("VARIABLE_IN_COMMAND2"),
EnvMode::LOCAL,
L!("at").to_owned(),
);
let _cleanup = ScopeGuard::new((), |_| {
vars.remove(L!("VARIABLE_IN_COMMAND"), EnvMode::default());
vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvMode::default());
});
validate!(
("echo", fg(HighlightRole::command)),
("./foo", param_valid_path),
("&", fg(HighlightRole::statement_terminator)),
);
validate!(
("command", fg(HighlightRole::keyword)),
("echo", fg(HighlightRole::command)),
("abc", fg(HighlightRole::param)),
("foo", param_valid_path),
("&", fg(HighlightRole::statement_terminator)),
);
validate!(
("echo", fg(HighlightRole::command)),
("foo&bar", fg(HighlightRole::param)),
("foo", fg(HighlightRole::param), ns),
("&", fg(HighlightRole::statement_terminator)),
("echo", fg(HighlightRole::command)),
("&>", fg(HighlightRole::redirection)),
);
validate!(
("if command", fg(HighlightRole::keyword)),
("ls", fg(HighlightRole::command)),
("; ", fg(HighlightRole::statement_terminator)),
("echo", fg(HighlightRole::command)),
("abc", fg(HighlightRole::param)),
("; ", fg(HighlightRole::statement_terminator)),
("/bin/definitely_not_a_command", fg(HighlightRole::error)),
("; ", fg(HighlightRole::statement_terminator)),
("end", fg(HighlightRole::keyword)),
);
// Verify that cd shows errors for non-directories.
validate!(
("cd", fg(HighlightRole::command)),
("dir", param_valid_path),
);
validate!(
("cd", fg(HighlightRole::command)),
("foo", fg(HighlightRole::error)),
);
validate!(
("cd", fg(HighlightRole::command)),
("--help", fg(HighlightRole::option)),
("-h", fg(HighlightRole::option)),
("definitely_not_a_directory", fg(HighlightRole::error)),
);
validate!(
("cd", fg(HighlightRole::command)),
("dir-in-cdpath", param_valid_path),
);
// Command substitutions.
validate!(
("echo", fg(HighlightRole::command)),
("param1", fg(HighlightRole::param)),
("-l", fg(HighlightRole::option)),
("--", fg(HighlightRole::option)),
("-l", fg(HighlightRole::param)),
("(", fg(HighlightRole::operat)),
("ls", fg(HighlightRole::command)),
("-l", fg(HighlightRole::option)),
("--", fg(HighlightRole::option)),
("-l", fg(HighlightRole::param)),
("param2", fg(HighlightRole::param)),
(")", fg(HighlightRole::operat)),
("|", fg(HighlightRole::statement_terminator)),
("cat", fg(HighlightRole::command)),
);
validate!(
("true", fg(HighlightRole::command)),
("$(", fg(HighlightRole::operat)),
("true", fg(HighlightRole::command)),
(")", fg(HighlightRole::operat)),
);
validate!(
("true", fg(HighlightRole::command)),
("\"before", fg(HighlightRole::quote)),
("$(", fg(HighlightRole::operat)),
("true", fg(HighlightRole::command)),
("param1", fg(HighlightRole::param)),
(")", fg(HighlightRole::operat)),
("after\"", fg(HighlightRole::quote)),
("param2", fg(HighlightRole::param)),
);
validate!(
("true", fg(HighlightRole::command)),
("\"", fg(HighlightRole::error)),
("unclosed quote", fg(HighlightRole::quote)),
("$(", fg(HighlightRole::operat)),
("true", fg(HighlightRole::command)),
(")", fg(HighlightRole::operat)),
);
// Redirections substitutions.
validate!(
("echo", fg(HighlightRole::command)),
("param1", fg(HighlightRole::param)),
// Input redirection.
("<", fg(HighlightRole::redirection)),
("/bin/echo", fg(HighlightRole::redirection)),
// Output redirection to a valid fd.
("1>&2", fg(HighlightRole::redirection)),
// Output redirection to an invalid fd.
("2>&", fg(HighlightRole::redirection)),
("LO", fg(HighlightRole::error)),
// Just a param, not a redirection.
("test/blah", fg(HighlightRole::param)),
// Input redirection from directory.
("<", fg(HighlightRole::redirection)),
("test/", fg(HighlightRole::error)),
// Output redirection to an invalid path.
("3>", fg(HighlightRole::redirection)),
("/not/a/valid/path/nope", fg(HighlightRole::error)),
// Output redirection to directory.
("3>", fg(HighlightRole::redirection)),
("test/nope/", fg(HighlightRole::error)),
// Redirections to overflow fd.
("99999999999999999999>&2", fg(HighlightRole::error)),
("2>&", fg(HighlightRole::redirection)),
("99999999999999999999", fg(HighlightRole::error)),
// Output redirection containing a command substitution.
("4>", fg(HighlightRole::redirection)),
("(", fg(HighlightRole::operat)),
("echo", fg(HighlightRole::command)),
("test/somewhere", fg(HighlightRole::param)),
(")", fg(HighlightRole::operat)),
// Just another param.
("param2", fg(HighlightRole::param)),
);
validate!(
("for", fg(HighlightRole::keyword)),
("x", fg(HighlightRole::param)),
("in", fg(HighlightRole::keyword)),
("set-by-for-1", fg(HighlightRole::param)),
("set-by-for-2", fg(HighlightRole::param)),
(";", fg(HighlightRole::statement_terminator)),
("echo", fg(HighlightRole::command)),
(">", fg(HighlightRole::redirection)),
("$x", fg(HighlightRole::redirection)),
(";", fg(HighlightRole::statement_terminator)),
("end", fg(HighlightRole::keyword)),
);
validate!(
("set", fg(HighlightRole::command)),
("x", fg(HighlightRole::param)),
("set-by-set", fg(HighlightRole::param)),
(";", fg(HighlightRole::statement_terminator)),
("echo", fg(HighlightRole::command)),
(">", fg(HighlightRole::redirection)),
("$x", fg(HighlightRole::redirection)),
("2>", fg(HighlightRole::redirection)),
("$totally_not_x", fg(HighlightRole::error)),
("<", fg(HighlightRole::redirection)),
("$x_but_its_an_impostor", fg(HighlightRole::error)),
);
validate!(
("x", fg(HighlightRole::param), ns),
("=", fg(HighlightRole::operat), ns),
("set-by-variable-override", fg(HighlightRole::param), ns),
("echo", fg(HighlightRole::command)),
(">", fg(HighlightRole::redirection)),
("$x", fg(HighlightRole::redirection)),
);
validate!(
("end", fg(HighlightRole::error)),
(";", fg(HighlightRole::statement_terminator)),
("if", fg(HighlightRole::keyword)),
("end", fg(HighlightRole::error)),
);
validate!(
("echo", fg(HighlightRole::command)),
("'", fg(HighlightRole::error)),
("single_quote", fg(HighlightRole::quote)),
("$stuff", fg(HighlightRole::quote)),
);
validate!(
("echo", fg(HighlightRole::command)),
("\"", fg(HighlightRole::error)),
("double_quote", fg(HighlightRole::quote)),
("$stuff", fg(HighlightRole::operat)),
);
validate!(
("echo", fg(HighlightRole::command)),
("$foo", fg(HighlightRole::operat)),
("\"", fg(HighlightRole::quote)),
("$bar", fg(HighlightRole::operat)),
("\"", fg(HighlightRole::quote)),
("$baz[", fg(HighlightRole::operat)),
("1 2..3", fg(HighlightRole::param)),
("]", fg(HighlightRole::operat)),
);
validate!(
("for", fg(HighlightRole::keyword)),
("i", fg(HighlightRole::param)),
("in", fg(HighlightRole::keyword)),
("1 2 3", fg(HighlightRole::param)),
(";", fg(HighlightRole::statement_terminator)),
("end", fg(HighlightRole::keyword)),
);
validate!(
("echo", fg(HighlightRole::command)),
("$$foo[", fg(HighlightRole::operat)),
("1", fg(HighlightRole::param)),
("][", fg(HighlightRole::operat)),
("2", fg(HighlightRole::param)),
("]", fg(HighlightRole::operat)),
("[3]", fg(HighlightRole::param)), // two dollar signs, so last one is not an expansion
);
validate!(
("cat", fg(HighlightRole::command)),
("/dev/null", param_valid_path),
("|", fg(HighlightRole::statement_terminator)),
// This is bogus, but we used to use "less" here and that doesn't have to be installed.
("cat", fg(HighlightRole::command)),
("2>", fg(HighlightRole::redirection)),
);
// Highlight path-prefixes only at the cursor.
validate!(
("cat", fg(HighlightRole::command)),
("/dev/nu", fg(HighlightRole::param)),
("/dev/nu", param_valid_path),
);
validate!(
("if", fg(HighlightRole::keyword)),
("true", fg(HighlightRole::command)),
("&&", fg(HighlightRole::operat)),
("false", fg(HighlightRole::command)),
(";", fg(HighlightRole::statement_terminator)),
("or", fg(HighlightRole::operat)),
("false", fg(HighlightRole::command)),
("||", fg(HighlightRole::operat)),
("true", fg(HighlightRole::command)),
(";", fg(HighlightRole::statement_terminator)),
("and", fg(HighlightRole::operat)),
("not", fg(HighlightRole::operat)),
("!", fg(HighlightRole::operat)),
("true", fg(HighlightRole::command)),
(";", fg(HighlightRole::statement_terminator)),
("end", fg(HighlightRole::keyword)),
);
validate!(
("echo", fg(HighlightRole::command)),
("%self", fg(HighlightRole::operat)),
("not%self", fg(HighlightRole::param)),
("self%not", fg(HighlightRole::param)),
);
validate!(
("false", fg(HighlightRole::command)),
("&|", fg(HighlightRole::statement_terminator)),
("true", fg(HighlightRole::command)),
);
validate!(
("HOME", fg(HighlightRole::param)),
("=", fg(HighlightRole::operat), ns),
(".", fg(HighlightRole::param), ns),
("VAR1", fg(HighlightRole::param)),
("=", fg(HighlightRole::operat), ns),
("VAL1", fg(HighlightRole::param), ns),
("VAR", fg(HighlightRole::param)),
("=", fg(HighlightRole::operat), ns),
("false", fg(HighlightRole::command)),
("|&", fg(HighlightRole::error)),
("true", fg(HighlightRole::command)),
("stuff", fg(HighlightRole::param)),
);
validate!(
("echo", fg(HighlightRole::command)), // (
(")", fg(HighlightRole::error)),
);
validate!(
("echo", fg(HighlightRole::command)),
("stuff", fg(HighlightRole::param)),
("# comment", fg(HighlightRole::comment)),
);
validate!(
("echo", fg(HighlightRole::command)),
("--", fg(HighlightRole::option)),
("-s", fg(HighlightRole::param)),
);
// Overlong paths don't crash (#7837).
let overlong = get_overlong_path();
validate!(
("touch", fg(HighlightRole::command)),
(&overlong, fg(HighlightRole::param)),
);
validate!(
("a", fg(HighlightRole::param)),
("=", fg(HighlightRole::operat), ns),
);
// Highlighting works across escaped line breaks (#8444).
validate!(
("echo", fg(HighlightRole::command)),
("$FISH_\\\n", fg(HighlightRole::operat)),
("VERSION", fg(HighlightRole::operat), ns),
);
validate!(
("/bin/ca", fg(HighlightRole::command), ns),
("*", fg(HighlightRole::operat), ns)
);
validate!(
("/bin/c", fg(HighlightRole::command), ns),
("*", fg(HighlightRole::operat), ns)
);
validate!(
("/bin/c", fg(HighlightRole::command), ns),
("{$VARIABLE_IN_COMMAND}", fg(HighlightRole::operat), ns),
("*", fg(HighlightRole::operat), ns)
);
validate!(
("/bin/c", fg(HighlightRole::command), ns),
("$VARIABLE_IN_COMMAND2", fg(HighlightRole::operat), ns)
);
validate!(("$EMPTY_VARIABLE", fg(HighlightRole::error)));
validate!(("\"$EMPTY_VARIABLE\"", fg(HighlightRole::error)));
validate!(
("echo", fg(HighlightRole::command)),
("\\UFDFD", fg(HighlightRole::escape)),
);
validate!(
("echo", fg(HighlightRole::command)),
("\\U10FFFF", fg(HighlightRole::escape)),
);
validate!(
("echo", fg(HighlightRole::command)),
("\\U110000", fg(HighlightRole::error)),
);
validate!(
(">", fg(HighlightRole::error)),
("echo", fg(HighlightRole::error)),
);
}