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:
parent
3ac4dbddc2
commit
3d87381f55
8 changed files with 170 additions and 42 deletions
25
.github/workflows/code-quality.yml
vendored
Normal file
25
.github/workflows/code-quality.yml
vendored
Normal 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
|
|
@ -5,8 +5,9 @@ use parking_lot::Mutex;
|
||||||
|
|
||||||
use super::COMMAND_LIST;
|
use super::COMMAND_LIST;
|
||||||
use crate::core::repl::exec::evaluate_command;
|
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 -
|
// increasing this value WILL cause a stack overflow
|
||||||
// Caz
|
// attempt at your own risk - Caz
|
||||||
|
const MAX_RECURSION_DEPTH: usize = 500;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref RECURSION_DEPTH: Mutex<usize> = parking_lot::Mutex::new(0);
|
static ref RECURSION_DEPTH: Mutex<usize> = parking_lot::Mutex::new(0);
|
||||||
|
|
|
@ -9,9 +9,9 @@ use log::debug;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustyline::{
|
use rustyline::{
|
||||||
error::ReadlineError, highlight::Highlighter, hint::HistoryHinter, history::DefaultHistory,
|
completion::Completer, error::ReadlineError, highlight::Highlighter, hint::HistoryHinter,
|
||||||
Cmd, Completer, ConditionalEventHandler, Editor, Event, EventContext, EventHandler, Helper,
|
history::DefaultHistory, Cmd, Completer, ConditionalEventHandler, Editor, Event, EventContext,
|
||||||
Hinter, KeyEvent, RepeatCount, Validator,
|
EventHandler, Helper, Hinter, KeyEvent, RepeatCount, Validator,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -19,8 +19,44 @@ use crate::{
|
||||||
utils::logger::LOGGER,
|
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)]
|
#[derive(Completer, Helper, Hinter, Validator)]
|
||||||
struct MyHelper(#[rustyline(Hinter)] HistoryHinter);
|
struct MyHelper {
|
||||||
|
#[rustyline(Hinter)]
|
||||||
|
hinter: HistoryHinter,
|
||||||
|
#[rustyline(Completer)]
|
||||||
|
completer: CommandCompleter,
|
||||||
|
}
|
||||||
|
|
||||||
impl Highlighter for MyHelper {
|
impl Highlighter for MyHelper {
|
||||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
|
||||||
|
@ -142,6 +178,11 @@ fn tokenize(command: &str) -> Vec<String> {
|
||||||
tokens
|
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<()> {
|
pub fn evaluate_command(input: &str) -> anyhow::Result<()> {
|
||||||
if input.trim().is_empty() {
|
if input.trim().is_empty() {
|
||||||
println!("Empty command, skipping. type 'help' for a list of commands.");
|
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<()> {
|
pub async fn handle_repl() -> anyhow::Result<()> {
|
||||||
let mut rl = Editor::<MyHelper, DefaultHistory>::new()?;
|
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(
|
rl.bind_sequence(
|
||||||
KeyEvent::from('`'),
|
KeyEvent::from('`'),
|
||||||
|
|
|
@ -26,8 +26,22 @@ pub struct Command {
|
||||||
function: Callable,
|
function: Callable,
|
||||||
pub arg_count: u8,
|
pub arg_count: u8,
|
||||||
}
|
}
|
||||||
|
#[allow(private_interfaces)]
|
||||||
impl Command {
|
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<()> {
|
pub fn execute(&self, args: Option<Vec<String>>) -> anyhow::Result<()> {
|
||||||
match &self.function {
|
match &self.function {
|
||||||
Callable::Simple(f) => {
|
Callable::Simple(f) => {
|
||||||
|
@ -69,21 +83,63 @@ pub struct CommandList {
|
||||||
pub aliases: RwLock<HashMap<String, String>>,
|
pub aliases: RwLock<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_similarity(target: &str, strings: &[String]) -> Option<String> {
|
fn hamming_distance(a: &str, b: &str) -> Option<usize> {
|
||||||
strings
|
if a.len() != b.len() {
|
||||||
.iter()
|
return None;
|
||||||
.filter(|s| target.chars().zip(s.chars()).any(|(c1, c2)| c1 == c2))
|
}
|
||||||
.min_by_key(|s| {
|
Some(
|
||||||
let mut diff_count = 0;
|
a.chars()
|
||||||
for (c1, c2) in target.chars().zip(s.chars()) {
|
.zip(b.chars())
|
||||||
if c1 != c2 {
|
.filter(|(char_a, char_b)| char_a != char_b)
|
||||||
diff_count += 1;
|
.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 {
|
impl CommandList {
|
||||||
|
@ -153,6 +209,13 @@ impl CommandList {
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
(expected, None) => {
|
||||||
|
eprintln!(
|
||||||
|
"Command: '{}' expected {} arguments but received none",
|
||||||
|
name, expected
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
(_, _) => command.execute(args),
|
(_, _) => command.execute(args),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -172,7 +235,10 @@ impl CommandList {
|
||||||
eprintln!("Did you mean: '{}'?", similar.green().italic().bold());
|
eprintln!("Did you mean: '{}'?", similar.green().italic().bold());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
None => Ok(()),
|
None => {
|
||||||
|
println!("Type 'help' for a list of commands");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#![deny(clippy::unwrap_in_result)]
|
#![deny(clippy::unwrap_in_result)]
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use plugin_api::plugin_imports::*;
|
use plugin_api::plugin_imports::*;
|
||||||
|
|
|
@ -30,7 +30,6 @@ impl DynamicLogger {
|
||||||
pub fn write_to_file(&self, file_path: &str) {
|
pub fn write_to_file(&self, file_path: &str) {
|
||||||
let file = OpenOptions::new()
|
let file = OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
|
|
||||||
.append(true)
|
.append(true)
|
||||||
.open(file_path)
|
.open(file_path)
|
||||||
.expect("Failed to open log file");
|
.expect("Failed to open log 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
15
test.zensh
15
test.zensh
|
@ -1,4 +1,11 @@
|
||||||
echo ping
|
|
||||||
echo pong
|
|
||||||
exec test.zensh
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo "Hello World"
|
||||||
|
echo "hello world"; hello
|
Loading…
Add table
Add a link
Reference in a new issue