From 3aba3aea1cbc9b52ca02f0ce055e5a1e50a6500f Mon Sep 17 00:00:00 2001 From: Cazdotsys Date: Mon, 9 Dec 2024 20:28:28 -0500 Subject: [PATCH] Improve repl autocorrect and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Combine comparison algorithims for autocorrect * clear zephyr functions * remove redundant comments because co-pilot is stupid and i probably will never try to use it again * implement basic tab completion * fix unused items * Make workflow check code quality * split code quality into its own file * make action fail on bad formatting * change workflow to nightly * f it, code quality is considered breaking * fix forgetting to set toolchain back to nightly when rewriting workflow (😔) * Add condition for too little arguments * run cargo fmt * remove unneeded feature directive --- .github/workflows/code-quality.yml | 25 ++++++++ engine/src/core/repl/commands.rs | 5 +- engine/src/core/repl/exec.rs | 54 +++++++++++++++-- engine/src/core/repl/mod.rs | 96 +++++++++++++++++++++++++----- engine/src/main.rs | 1 - engine/src/utils/logger.rs | 1 - subcrates/zephyr/src/lib.rs | 13 ---- test.zensh | 15 +++-- 8 files changed, 169 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..763eaf5 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,25 @@ +name: Code Quality + +on: [push, pull_request] + +jobs: + code-quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: clippy, rustfmt + + - name: Check formatting + run: cargo fmt -- --check + + - name: Run Clippy + run: cargo clippy -- -D warnings + + - name: Compilation Check + run: cargo check \ No newline at end of file diff --git a/engine/src/core/repl/commands.rs b/engine/src/core/repl/commands.rs index d41fe5f..4c1a8d9 100644 --- a/engine/src/core/repl/commands.rs +++ b/engine/src/core/repl/commands.rs @@ -5,8 +5,9 @@ use parking_lot::Mutex; use super::COMMAND_LIST; use crate::core::repl::exec::evaluate_command; -const MAX_RECURSION_DEPTH: usize = 500; // increasing this value WILL cause a stack overflow. attempt at your own risk - - // Caz +// increasing this value WILL cause a stack overflow +// attempt at your own risk - Caz +const MAX_RECURSION_DEPTH: usize = 500; lazy_static! { static ref RECURSION_DEPTH: Mutex = parking_lot::Mutex::new(0); diff --git a/engine/src/core/repl/exec.rs b/engine/src/core/repl/exec.rs index eae0dc9..bda5252 100644 --- a/engine/src/core/repl/exec.rs +++ b/engine/src/core/repl/exec.rs @@ -9,9 +9,9 @@ use log::debug; use parking_lot::Mutex; use regex::Regex; use rustyline::{ - error::ReadlineError, highlight::Highlighter, hint::HistoryHinter, history::DefaultHistory, - Cmd, Completer, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, Helper, - Hinter, KeyEvent, RepeatCount, Validator, + completion::Completer, error::ReadlineError, highlight::Highlighter, hint::HistoryHinter, + history::DefaultHistory, Cmd, Completer, ConditionalEventHandler, Editor, Event, EventContext, + EventHandler, Helper, Hinter, KeyEvent, RepeatCount, Validator, }; use crate::{ @@ -19,8 +19,44 @@ use crate::{ utils::logger::LOGGER, }; +struct CommandCompleter; +impl CommandCompleter { + fn new() -> Self { + CommandCompleter {} + } +} + +impl Completer for CommandCompleter { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &rustyline::Context<'_>, + ) -> rustyline::Result<(usize, Vec)> { + let binding = COMMAND_LIST.commands.read(); + let filtered_commands: Vec<_> = binding + .iter() + .filter(|command| command.name.starts_with(line)) + .collect(); + + let completions: Vec = filtered_commands + .iter() + .filter(|command| command.name.starts_with(&line[..pos])) + .map(|command| command.name[pos..].to_string()) + .collect(); + Ok((pos, completions)) + } +} + #[derive(Completer, Helper, Hinter, Validator)] -struct MyHelper(#[rustyline(Hinter)] HistoryHinter); +struct MyHelper { + #[rustyline(Hinter)] + hinter: HistoryHinter, + #[rustyline(Completer)] + completer: CommandCompleter, +} impl Highlighter for MyHelper { fn highlight_prompt<'b, 's: 'b, 'p: 'b>( @@ -142,6 +178,11 @@ fn tokenize(command: &str) -> Vec { tokens } +pub fn parse_command(input: &str) -> anyhow::Result> { + let pattern = Regex::new(r"[;|\n]").unwrap(); + let commands: Vec = pattern.split(input).map(|s| String::from(s)).collect(); + Ok(commands) +} pub fn evaluate_command(input: &str) -> anyhow::Result<()> { if input.trim().is_empty() { println!("Empty command, skipping. type 'help' for a list of commands."); @@ -176,7 +217,10 @@ pub fn evaluate_command(input: &str) -> anyhow::Result<()> { pub async fn handle_repl() -> anyhow::Result<()> { let mut rl = Editor::::new()?; - rl.set_helper(Some(MyHelper(HistoryHinter::new()))); + rl.set_helper(Some(MyHelper { + hinter: HistoryHinter::new(), + completer: CommandCompleter::new(), + })); rl.bind_sequence( KeyEvent::from('`'), diff --git a/engine/src/core/repl/mod.rs b/engine/src/core/repl/mod.rs index 6a935bc..698eb82 100644 --- a/engine/src/core/repl/mod.rs +++ b/engine/src/core/repl/mod.rs @@ -26,8 +26,22 @@ pub struct Command { function: Callable, pub arg_count: u8, } - +#[allow(private_interfaces)] impl Command { + pub fn new( + name: &'static str, + description: Option<&'static str>, + function: Callable, + arg_count: Option, + ) -> Self { + Command { + name, + description, + function, + arg_count: arg_count.unwrap_or(0), + } + } + pub fn execute(&self, args: Option>) -> anyhow::Result<()> { match &self.function { Callable::Simple(f) => { @@ -69,21 +83,63 @@ pub struct CommandList { pub aliases: RwLock>, } +fn hamming_distance(a: &str, b: &str) -> Option { + if a.len() != b.len() { + return None; + } + Some( + a.chars() + .zip(b.chars()) + .filter(|(char_a, char_b)| char_a != char_b) + .count(), + ) +} + +fn edit_distance(a: &str, b: &str) -> usize { + let m = a.len(); + let n = b.len(); + + let mut dp = vec![vec![0; n + 1]; m + 1]; + + for i in 0..=m { + for j in 0..=n { + if i == 0 { + dp[i][j] = j; + } else if j == 0 { + dp[i][j] = i; + } else if a.chars().nth(i - 1) == b.chars().nth(j - 1) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + dp[i - 1][j - 1].min(dp[i - 1][j]).min(dp[i][j - 1]); + } + } + } + + dp[m][n] +} + fn check_similarity(target: &str, strings: &[String]) -> Option { - strings - .iter() - .filter(|s| target.chars().zip(s.chars()).any(|(c1, c2)| c1 == c2)) - .min_by_key(|s| { - let mut diff_count = 0; - for (c1, c2) in target.chars().zip(s.chars()) { - if c1 != c2 { - diff_count += 1; - } + let max_hamming_distance: usize = 2; + let max_edit_distance: usize = 2; + let mut best_match: Option = None; + let mut best_distance = usize::MAX; + + for s in strings { + if let Some(hamming_dist) = hamming_distance(target, s) { + if hamming_dist <= max_hamming_distance && hamming_dist < best_distance { + best_distance = hamming_dist; + best_match = Some(s.clone()); } - diff_count += target.len().abs_diff(s.len()); - diff_count - }) - .cloned() + } else { + let edit_dist = edit_distance(target, s); + if edit_dist <= max_edit_distance && edit_dist < best_distance { + best_distance = edit_dist; + best_match = Some(s.clone()); + } + } + } + + best_match } impl CommandList { @@ -153,6 +209,13 @@ impl CommandList { ); Ok(()) } + (expected, None) => { + eprintln!( + "Command: '{}' expected {} arguments but received none", + name, expected + ); + Ok(()) + } (_, _) => command.execute(args), } } else { @@ -172,7 +235,10 @@ impl CommandList { eprintln!("Did you mean: '{}'?", similar.green().italic().bold()); Ok(()) } - None => Ok(()), + None => { + println!("Type 'help' for a list of commands"); + Ok(()) + } } } } diff --git a/engine/src/main.rs b/engine/src/main.rs index 4b2dc0e..767db02 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -1,5 +1,4 @@ #![deny(clippy::unwrap_in_result)] - use anyhow::Result; use log::LevelFilter; use plugin_api::plugin_imports::*; diff --git a/engine/src/utils/logger.rs b/engine/src/utils/logger.rs index b927556..765f0e8 100644 --- a/engine/src/utils/logger.rs +++ b/engine/src/utils/logger.rs @@ -30,7 +30,6 @@ impl DynamicLogger { pub fn write_to_file(&self, file_path: &str) { let file = OpenOptions::new() .create(true) - .append(true) .open(file_path) .expect("Failed to open log file"); diff --git a/subcrates/zephyr/src/lib.rs b/subcrates/zephyr/src/lib.rs index b93cf3f..8b13789 100644 --- a/subcrates/zephyr/src/lib.rs +++ b/subcrates/zephyr/src/lib.rs @@ -1,14 +1 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/test.zensh b/test.zensh index 0fbb7d4..f7725fc 100644 --- a/test.zensh +++ b/test.zensh @@ -1,4 +1,11 @@ -echo ping -echo pong -exec test.zensh -break \ No newline at end of file + + + + + + + + + +echo "Hello World" +echo "hello world"; hello \ No newline at end of file