Improve repl autocorrect and error handling

* 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
This commit is contained in:
Chance 2024-12-09 20:28:28 -05:00 committed by BitSyndicate
parent 9088af58f0
commit a9f8b43f50
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: 443E4198D6BBA6DE
8 changed files with 170 additions and 42 deletions

25
.github/workflows/code-quality.yml vendored Normal file
View file

@ -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

View file

@ -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<usize> = parking_lot::Mutex::new(0);

View file

@ -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<Self::Candidate>)> {
let binding = COMMAND_LIST.commands.read();
let filtered_commands: Vec<_> = binding
.iter()
.filter(|command| command.name.starts_with(line))
.collect();
let completions: Vec<String> = 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<String> {
tokens
}
pub fn parse_command(input: &str) -> anyhow::Result<Vec<String>> {
let pattern = Regex::new(r"[;|\n]").unwrap();
let commands: Vec<String> = 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::<MyHelper, DefaultHistory>::new()?;
rl.set_helper(Some(MyHelper(HistoryHinter::new())));
rl.set_helper(Some(MyHelper {
hinter: HistoryHinter::new(),
completer: CommandCompleter::new(),
}));
rl.bind_sequence(
KeyEvent::from('`'),

View file

@ -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<u8>,
) -> Self {
Command {
name,
description,
function,
arg_count: arg_count.unwrap_or(0),
}
}
pub fn execute(&self, args: Option<Vec<String>>) -> anyhow::Result<()> {
match &self.function {
Callable::Simple(f) => {
@ -69,21 +83,63 @@ pub struct CommandList {
pub aliases: RwLock<HashMap<String, String>>,
}
fn check_similarity(target: &str, strings: &[String]) -> Option<String> {
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;
}
fn hamming_distance(a: &str, b: &str) -> Option<usize> {
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]);
}
diff_count += target.len().abs_diff(s.len());
diff_count
})
.cloned()
}
}
dp[m][n]
}
fn check_similarity(target: &str, strings: &[String]) -> Option<String> {
let max_hamming_distance: usize = 2;
let max_edit_distance: usize = 2;
let mut best_match: Option<String> = 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());
}
} 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(())
}
}
}
}

View file

@ -1,5 +1,4 @@
#![deny(clippy::unwrap_in_result)]
use anyhow::Result;
use log::LevelFilter;
use plugin_api::plugin_imports::*;

View file

@ -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");

View file

@ -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);
}
}

View file

@ -1,4 +1,11 @@
echo ping
echo pong
exec test.zensh
break
echo "Hello World"
echo "hello world"; hello