more unit tests, better highlighting
This commit is contained in:
@@ -427,7 +427,7 @@ impl LexStream {
|
|||||||
'$' if chars.peek() == Some(&'(') => {
|
'$' if chars.peek() == Some(&'(') => {
|
||||||
pos += 2;
|
pos += 2;
|
||||||
chars.next();
|
chars.next();
|
||||||
let mut paren_count = 0;
|
let mut paren_count = 1;
|
||||||
let paren_pos = pos;
|
let paren_pos = pos;
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
@@ -444,7 +444,7 @@ impl LexStream {
|
|||||||
')' => {
|
')' => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
paren_count -= 1;
|
paren_count -= 1;
|
||||||
if paren_count >= 0 {
|
if paren_count <= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -461,34 +461,38 @@ impl LexStream {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
let mut cmdsub_tk = self.get_token(self.cursor..pos, TkRule::Str);
|
||||||
|
cmdsub_tk.flags |= TkFlags::IS_CMDSUB;
|
||||||
|
self.cursor = pos;
|
||||||
|
return Ok(cmdsub_tk)
|
||||||
}
|
}
|
||||||
'(' if self.next_is_cmd() => {
|
'(' if self.next_is_cmd() => {
|
||||||
let mut paren_stack = vec!['('];
|
pos += 1;
|
||||||
|
let mut paren_count = 1;
|
||||||
let paren_pos = pos;
|
let paren_pos = pos;
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
pos += ch.len_utf8();
|
|
||||||
match ch {
|
match ch {
|
||||||
'\\' => {
|
'\\' => {
|
||||||
|
pos += 1;
|
||||||
if let Some(next_ch) = chars.next() {
|
if let Some(next_ch) = chars.next() {
|
||||||
pos += next_ch.len_utf8();
|
pos += next_ch.len_utf8();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'(' => {
|
'(' => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
paren_stack.push(ch);
|
paren_count += 1;
|
||||||
}
|
}
|
||||||
')' => {
|
')' => {
|
||||||
pos += 1;
|
pos += 1;
|
||||||
paren_stack.pop();
|
paren_count -= 1;
|
||||||
if paren_stack.is_empty() {
|
if paren_count <= 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => continue
|
_ => pos += ch.len_utf8()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !paren_stack.is_empty() && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
if paren_count != 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
|
||||||
self.cursor = pos;
|
|
||||||
return Err(
|
return Err(
|
||||||
ShErr::full(
|
ShErr::full(
|
||||||
ShErrKind::ParseErr,
|
ShErrKind::ParseErr,
|
||||||
@@ -502,6 +506,7 @@ impl LexStream {
|
|||||||
subsh_tk.flags |= TkFlags::IS_SUBSH;
|
subsh_tk.flags |= TkFlags::IS_SUBSH;
|
||||||
self.cursor = pos;
|
self.cursor = pos;
|
||||||
self.set_next_is_cmd(true);
|
self.set_next_is_cmd(true);
|
||||||
|
flog!(DEBUG, "returning subsh tk");
|
||||||
return Ok(subsh_tk)
|
return Ok(subsh_tk)
|
||||||
}
|
}
|
||||||
'{' if pos == self.cursor && self.next_is_cmd() => {
|
'{' if pos == self.cursor && self.next_is_cmd() => {
|
||||||
@@ -666,6 +671,8 @@ impl LexStream {
|
|||||||
impl Iterator for LexStream {
|
impl Iterator for LexStream {
|
||||||
type Item = ShResult<Tk>;
|
type Item = ShResult<Tk>;
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
flog!(DEBUG,self.cursor);
|
||||||
|
flog!(DEBUG,self.source.len());
|
||||||
assert!(self.cursor <= self.source.len());
|
assert!(self.cursor <= self.source.len());
|
||||||
// We are at the end of the input
|
// We are at the end of the input
|
||||||
if self.cursor == self.source.len() {
|
if self.cursor == self.source.len() {
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ impl ParsedSrc {
|
|||||||
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
|
pub fn parse_src(&mut self) -> Result<(),Vec<ShErr>> {
|
||||||
let mut tokens = vec![];
|
let mut tokens = vec![];
|
||||||
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
|
for lex_result in LexStream::new(self.src.clone(), LexFlags::empty()) {
|
||||||
flog!(DEBUG, lex_result);
|
|
||||||
match lex_result {
|
match lex_result {
|
||||||
Ok(token) => tokens.push(token),
|
Ok(token) => tokens.push(token),
|
||||||
Err(error) => return Err(vec![error])
|
Err(error) => return Err(vec![error])
|
||||||
@@ -1417,7 +1416,6 @@ impl Iterator for ParseStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let result = self.parse_cmd_list();
|
let result = self.parse_cmd_list();
|
||||||
flog!(DEBUG, result);
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Some(node)) => {
|
Ok(Some(node)) => {
|
||||||
Some(Ok(node))
|
Some(Ok(node))
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, sync::Arc};
|
use std::{env, mem, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, sync::Arc};
|
||||||
use crate::builtin::BUILTINS;
|
use crate::builtin::BUILTINS;
|
||||||
|
|
||||||
use rustyline::highlight::Highlighter;
|
use rustyline::highlight::Highlighter;
|
||||||
@@ -26,24 +26,48 @@ impl FernHighlighter {
|
|||||||
pub fn highlight_subsh(&self, token: Tk) -> String {
|
pub fn highlight_subsh(&self, token: Tk) -> String {
|
||||||
if token.flags.contains(TkFlags::IS_SUBSH) {
|
if token.flags.contains(TkFlags::IS_SUBSH) {
|
||||||
let raw = token.as_str();
|
let raw = token.as_str();
|
||||||
let body = &raw[1..raw.len() - 1];
|
Self::hl_subsh_raw(raw)
|
||||||
let sub_hl = FernHighlighter::new(body.to_string());
|
|
||||||
let body_highlighted = sub_hl.hl_input();
|
|
||||||
let open_paren = "(".styled(Style::BrightBlue);
|
|
||||||
let close_paren = ")".styled(Style::BrightBlue);
|
|
||||||
format!("{open_paren}{body_highlighted}{close_paren}")
|
|
||||||
} else if token.flags.contains(TkFlags::IS_CMDSUB) {
|
} else if token.flags.contains(TkFlags::IS_CMDSUB) {
|
||||||
let raw = token.as_str();
|
let raw = token.as_str();
|
||||||
let body = &raw[2..raw.len() - 1];
|
Self::hl_cmdsub_raw(raw)
|
||||||
let sub_hl = FernHighlighter::new(body.to_string());
|
|
||||||
let body_highlighted = sub_hl.hl_input();
|
|
||||||
let dollar_paren = "$(".styled(Style::BrightBlue);
|
|
||||||
let close_paren = ")".styled(Style::BrightBlue);
|
|
||||||
format!("{dollar_paren}{body_highlighted}{close_paren}")
|
|
||||||
} else {
|
} else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn hl_subsh_raw(raw: &str) -> String {
|
||||||
|
let mut body = &raw[1..];
|
||||||
|
let mut closed = false;
|
||||||
|
if body.ends_with(')') {
|
||||||
|
body = &body[..body.len() - 1];
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
let sub_hl = FernHighlighter::new(body.to_string());
|
||||||
|
let body_highlighted = sub_hl.hl_input();
|
||||||
|
let open_paren = "(".styled(Style::BrightBlue);
|
||||||
|
let close_paren = ")".styled(Style::BrightBlue);
|
||||||
|
let mut result = format!("{open_paren}{body_highlighted}");
|
||||||
|
if closed {
|
||||||
|
result.push_str(&close_paren);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
pub fn hl_cmdsub_raw(raw: &str) -> String {
|
||||||
|
let mut body = &raw[2..];
|
||||||
|
let mut closed = false;
|
||||||
|
if body.ends_with(')') {
|
||||||
|
body = &body[..body.len() - 1];
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
let sub_hl = FernHighlighter::new(body.to_string());
|
||||||
|
let body_highlighted = sub_hl.hl_input();
|
||||||
|
let dollar_paren = "$(".styled(Style::BrightBlue);
|
||||||
|
let close_paren = ")".styled(Style::BrightBlue);
|
||||||
|
let mut result = format!("{dollar_paren}{body_highlighted}");
|
||||||
|
if closed {
|
||||||
|
result.push_str(&close_paren);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
pub fn hl_command(&self, token: Tk) -> String {
|
pub fn hl_command(&self, token: Tk) -> String {
|
||||||
let raw = token.as_str();
|
let raw = token.as_str();
|
||||||
let paths = env::var("PATH")
|
let paths = env::var("PATH")
|
||||||
@@ -78,11 +102,82 @@ impl FernHighlighter {
|
|||||||
raw.styled(Style::Bold | Style::Red)
|
raw.styled(Style::Bold | Style::Red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn hl_dquote(&self, token: Tk) -> String {
|
||||||
|
let raw = token.as_str();
|
||||||
|
let mut chars = raw.chars().peekable();
|
||||||
|
const YELLOW: &str = "\x1b[33m";
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
let mut result = String::new();
|
||||||
|
let mut dquote_count = 0;
|
||||||
|
|
||||||
|
result.push_str(YELLOW);
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
match ch {
|
||||||
|
'\\' => {
|
||||||
|
result.push(ch);
|
||||||
|
if let Some(ch) = chars.next() {
|
||||||
|
result.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'"' => {
|
||||||
|
dquote_count += 1;
|
||||||
|
result.push(ch);
|
||||||
|
if dquote_count >= 2 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'$' if chars.peek() == Some(&'(') => {
|
||||||
|
let mut raw_cmd_sub = String::new();
|
||||||
|
raw_cmd_sub.push(ch);
|
||||||
|
raw_cmd_sub.push(chars.next().unwrap());
|
||||||
|
let mut cmdsub_count = 1;
|
||||||
|
|
||||||
|
while let Some(cmdsub_ch) = chars.next() {
|
||||||
|
match cmdsub_ch {
|
||||||
|
'\\' => {
|
||||||
|
raw_cmd_sub.push(cmdsub_ch);
|
||||||
|
if let Some(ch) = chars.next() {
|
||||||
|
raw_cmd_sub.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'$' if chars.peek() == Some(&'(') => {
|
||||||
|
cmdsub_count += 1;
|
||||||
|
raw_cmd_sub.push(cmdsub_ch);
|
||||||
|
raw_cmd_sub.push(chars.next().unwrap());
|
||||||
|
}
|
||||||
|
')' => {
|
||||||
|
cmdsub_count -= 1;
|
||||||
|
raw_cmd_sub.push(cmdsub_ch);
|
||||||
|
if cmdsub_count <= 0 {
|
||||||
|
let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub));
|
||||||
|
result.push_str(&styled);
|
||||||
|
result.push_str(YELLOW);
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => raw_cmd_sub.push(cmdsub_ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !raw_cmd_sub.is_empty() {
|
||||||
|
let styled = Self::hl_cmdsub_raw(&mem::take(&mut raw_cmd_sub));
|
||||||
|
result.push_str(&styled);
|
||||||
|
result.push_str(YELLOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => result.push(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_str(RESET);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
pub fn hl_input(&self) -> String {
|
pub fn hl_input(&self) -> String {
|
||||||
let mut output = self.input.clone();
|
let mut output = self.input.clone();
|
||||||
|
|
||||||
// TODO: properly implement highlighting for unfinished input
|
// TODO: properly implement highlighting for unfinished input
|
||||||
let lex_results = LexStream::new(Arc::new(output.clone()), LexFlags::empty());
|
let lex_results = LexStream::new(Arc::new(output.clone()), LexFlags::LEX_UNFINISHED);
|
||||||
let mut tokens = vec![];
|
let mut tokens = vec![];
|
||||||
|
|
||||||
for result in lex_results {
|
for result in lex_results {
|
||||||
@@ -107,6 +202,9 @@ impl FernHighlighter {
|
|||||||
if token.flags.contains(TkFlags::IS_CMD) {
|
if token.flags.contains(TkFlags::IS_CMD) {
|
||||||
let styled = self.hl_command(token.clone());
|
let styled = self.hl_command(token.clone());
|
||||||
output.replace_range(token.span.start..token.span.end, &styled);
|
output.replace_range(token.span.start..token.span.end, &styled);
|
||||||
|
} else if is_dquote(&token) {
|
||||||
|
let styled = self.hl_dquote(token.clone());
|
||||||
|
output.replace_range(token.span.start..token.span.end, &styled);
|
||||||
} else {
|
} else {
|
||||||
output.replace_range(token.span.start..token.span.end, &token.to_string());
|
output.replace_range(token.span.start..token.span.end, &token.to_string());
|
||||||
}
|
}
|
||||||
@@ -143,3 +241,8 @@ impl Highlighter for FernReadline {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_dquote(token: &Tk) -> bool {
|
||||||
|
let raw = token.as_str();
|
||||||
|
raw.starts_with('"')
|
||||||
|
}
|
||||||
|
|||||||
27
src/tests/highlight.rs
Normal file
27
src/tests/highlight.rs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
|
use crate::prompt::highlight::FernHighlighter;
|
||||||
|
|
||||||
|
use super::super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn highlight_simple() {
|
||||||
|
let line = "echo foo bar";
|
||||||
|
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
||||||
|
assert_snapshot!(styled)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn highlight_cmd_sub() {
|
||||||
|
let line = "echo foo $(echo bar)";
|
||||||
|
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
||||||
|
assert_snapshot!(styled)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn highlight_cmd_sub_in_dquotes() {
|
||||||
|
let line = "echo \"foo $(echo bar) biz\"";
|
||||||
|
let styled = FernHighlighter::new(line.to_string()).hl_input();
|
||||||
|
assert_snapshot!(styled)
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ pub mod expand;
|
|||||||
pub mod term;
|
pub mod term;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod getopt;
|
pub mod getopt;
|
||||||
|
pub mod script;
|
||||||
|
pub mod highlight;
|
||||||
|
|
||||||
/// Unsafe to use outside of tests
|
/// Unsafe to use outside of tests
|
||||||
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
|
pub fn get_nodes<F1>(input: &str, filter: F1) -> Vec<Node>
|
||||||
|
|||||||
52
src/tests/script.rs
Normal file
52
src/tests/script.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use std::process::{self, Output};
|
||||||
|
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::super::*;
|
||||||
|
fn get_script_output(name: &str, args: &[&str]) -> Output {
|
||||||
|
// Resolve the path to the fern binary.
|
||||||
|
// Do not question me.
|
||||||
|
let mut fern_path = env::current_exe()
|
||||||
|
.expect("Failed to get test executable"); // The path to the test executable
|
||||||
|
fern_path.pop(); // Hocus pocus
|
||||||
|
fern_path.pop();
|
||||||
|
fern_path.push("fern"); // Abra Kadabra
|
||||||
|
|
||||||
|
if !fern_path.is_file() {
|
||||||
|
fern_path.pop();
|
||||||
|
fern_path.pop();
|
||||||
|
fern_path.push("release");
|
||||||
|
fern_path.push("fern");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fern_path.is_file() {
|
||||||
|
panic!("where the hell is the binary")
|
||||||
|
}
|
||||||
|
|
||||||
|
process::Command::new(fern_path) // Alakazam
|
||||||
|
.arg(name)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.expect("Failed to run script")
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn script_hello_world() {
|
||||||
|
let output = get_script_output("./test_scripts/hello.sh", &[]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert_eq!(stdout.trim(), "Hello, World!")
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn script_cmdsub() {
|
||||||
|
let output = get_script_output("./test_scripts/cmdsub.sh", &[]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert_eq!(stdout.trim(), "foo Hello bar")
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn script_multiline() {
|
||||||
|
let output = get_script_output("./test_scripts/multiline.sh", &[]);
|
||||||
|
assert!(output.status.success());
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert_eq!(stdout.trim(), "foo\nbar\nbiz\nbuzz")
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ source: src/tests/error.rs
|
|||||||
expression: err_fmt
|
expression: err_fmt
|
||||||
---
|
---
|
||||||
[31m[1mParse Error[0m - Unclosed subshell
|
[31m[1mParse Error[0m - Unclosed subshell
|
||||||
[36m[1m->[0m [[36m[1m1[0m;[36m[1m1[0m]
|
[36m[1m->[0m [[36m[1m1[0m;[36m[1m2[0m]
|
||||||
[36m[1m |[0m
|
[36m[1m |[0m
|
||||||
[36m[1m1 |[0m (foo
|
[36m[1m1 |[0m (foo
|
||||||
[36m[1m |[0m [31m[1m^[0m
|
[36m[1m |[0m [31m[1m^[0m
|
||||||
[36m[1m |[0m
|
[36m[1m |[0m
|
||||||
|
|||||||
1
test_scripts/cmdsub.sh
Executable file
1
test_scripts/cmdsub.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
echo "foo $(echo "$(echo "$(echo "$(echo Hello)")")") bar"
|
||||||
1
test_scripts/hello.sh
Executable file
1
test_scripts/hello.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
echo Hello, World!
|
||||||
4
test_scripts/multiline.sh
Normal file
4
test_scripts/multiline.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
echo foo
|
||||||
|
echo bar
|
||||||
|
echo biz
|
||||||
|
echo buzz
|
||||||
Reference in New Issue
Block a user