diff --git a/Cargo.lock b/Cargo.lock index 2ac962772..d78a65543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,6 +374,11 @@ dependencies = [ "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "exitcode" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "failure" version = "0.1.5" @@ -1166,6 +1171,7 @@ dependencies = [ "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)", "crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "exitcode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", "flame 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", "flamer 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "flate2 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)", @@ -2055,6 +2061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b" "checksum ena 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f56c93cc076508c549d9bb747f79aa9b4eb098be7b8cad8830c3137ef52d1e00" "checksum env_logger 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" +"checksum exitcode 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" "checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" diff --git a/src/main.rs b/src/main.rs index e7d97bdcd..6291d61fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,10 +9,10 @@ use rustpython_compiler::{compile, error::CompileError, error::CompileErrorType} use rustpython_parser::error::ParseErrorType; use rustpython_vm::{ import, match_class, - obj::{objint::PyInt, objstr::PyStringRef, objtuple::PyTuple, objtype}, + obj::{objint::PyInt, objtuple::PyTuple, objtype}, print_exception, - pyobject::{ItemProtocol, PyIterable, PyObjectRef, PyResult, TryFromObject}, - scope::{NameProtocol, Scope}, + pyobject::{ItemProtocol, PyObjectRef, PyResult}, + scope::Scope, util, PySettings, VirtualMachine, }; use std::convert::TryInto; @@ -22,6 +22,8 @@ use std::path::PathBuf; use std::process; use std::str::FromStr; +mod shell_helper; + fn main() { #[cfg(feature = "flame-it")] let main_guard = flame::start_guard("RustPython main"); @@ -487,154 +489,6 @@ fn shell_exec(vm: &VirtualMachine, source: &str, scope: Scope) -> ShellExecResul } } -struct ShellHelper<'a> { - vm: &'a VirtualMachine, - scope: Scope, -} - -impl ShellHelper<'_> { - fn complete_opt(&self, line: &str) -> Option<(usize, Vec)> { - let mut words = vec![String::new()]; - fn revlastword(words: &mut Vec) { - let word = words.last_mut().unwrap(); - let revword = word.chars().rev().collect(); - *word = revword; - } - let mut startpos = 0; - for (i, c) in line.chars().rev().enumerate() { - match c { - '.' => { - // check for a double dot - if i != 0 && words.last().map_or(false, |s| s.is_empty()) { - return None; - } - revlastword(&mut words); - if words.len() == 1 { - startpos = line.len() - i; - } - words.push(String::new()); - } - c if c.is_alphanumeric() || c == '_' => words.last_mut().unwrap().push(c), - _ => { - if words.len() == 1 { - if words.last().unwrap().is_empty() { - return None; - } - startpos = line.len() - i; - } - break; - } - } - } - revlastword(&mut words); - words.reverse(); - - // the very first word and then all the ones after the dot - let (first, rest) = words.split_first().unwrap(); - - let str_iter = |obj| { - PyIterable::::try_from_object(self.vm, obj) - .ok()? - .iter(self.vm) - .ok() - }; - - type StrIter<'a> = Box> + 'a>; - - let (iter, prefix) = if let Some((last, parents)) = rest.split_last() { - // we need to get an attribute based off of the dir() of an object - - // last: the last word, could be empty if it ends with a dot - // parents: the words before the dot - - let mut current = self.scope.load_global(self.vm, first)?; - - for attr in parents { - current = self.vm.get_attribute(current.clone(), attr.as_str()).ok()?; - } - - ( - Box::new(str_iter( - self.vm.call_method(¤t, "__dir__", vec![]).ok()?, - )?) as StrIter, - last.as_str(), - ) - } else { - // we need to get a variable based off of globals/builtins - - let globals = str_iter( - self.vm - .call_method(self.scope.globals.as_object(), "keys", vec![]) - .ok()?, - )?; - let iter = if first.as_str().is_empty() { - // only show globals that don't start with a '_' - Box::new(globals.filter(|r| { - r.as_ref() - .ok() - .map_or(true, |s| !s.as_str().starts_with('_')) - })) as StrIter - } else { - // show globals and builtins - Box::new( - globals.chain(str_iter( - self.vm - .call_method(&self.vm.builtins, "__dir__", vec![]) - .ok()?, - )?), - ) as StrIter - }; - (iter, first.as_str()) - }; - let completions = iter - .filter(|res| { - res.as_ref() - .ok() - .map_or(true, |s| s.as_str().starts_with(prefix)) - }) - .collect::, _>>() - .ok()?; - let no_underscore = completions - .iter() - .cloned() - .filter(|s| !prefix.starts_with('_') && !s.as_str().starts_with('_')) - .collect::>(); - let mut completions = if no_underscore.is_empty() { - completions - } else { - no_underscore - }; - completions.sort_by(|a, b| std::cmp::Ord::cmp(a.as_str(), b.as_str())); - Some(( - startpos, - completions - .into_iter() - .map(|s| s.as_str().to_owned()) - .collect(), - )) - } -} - -impl rustyline::completion::Completer for ShellHelper<'_> { - type Candidate = String; - - fn complete( - &self, - line: &str, - pos: usize, - _ctx: &rustyline::Context, - ) -> rustyline::Result<(usize, Vec)> { - if pos != line.len() { - return Ok((0, vec![])); - } - Ok(self.complete_opt(line).unwrap_or((0, vec![]))) - } -} - -impl rustyline::hint::Hinter for ShellHelper<'_> {} -impl rustyline::highlight::Highlighter for ShellHelper<'_> {} -impl rustyline::Helper for ShellHelper<'_> {} - #[cfg(not(target_os = "wasi"))] fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { use rustyline::{error::ReadlineError, CompletionType, Config, Editor}; @@ -650,10 +504,7 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { .completion_type(CompletionType::List) .build(), ); - repl.set_helper(Some(ShellHelper { - vm, - scope: scope.clone(), - })); + repl.set_helper(Some(shell_helper::ShellHelper::new(vm, scope.clone()))); let mut full_input = String::new(); // Retrieve a `history_path_str` dependent on the OS diff --git a/src/shell_helper.rs b/src/shell_helper.rs index a20f76275..ac1e2d5cd 100644 --- a/src/shell_helper.rs +++ b/src/shell_helper.rs @@ -4,8 +4,8 @@ use rustpython_vm::scope::{NameProtocol, Scope}; use rustpython_vm::VirtualMachine; use rustyline::{completion::Completer, highlight::Highlighter, hint::Hinter, Context, Helper}; -pub struct ShellHelper<'a> { - vm: &'a VirtualMachine, +pub struct ShellHelper<'vm> { + vm: &'vm VirtualMachine, scope: Scope, } @@ -14,7 +14,7 @@ fn reverse_string(s: &mut String) { *s = rev; } -fn extract_words(line: &str) -> Option<(usize, Vec)> { +fn split_idents_on_dot(line: &str) -> Option<(usize, Vec)> { let mut words = vec![String::new()]; let mut startpos = 0; for (i, c) in line.chars().rev().enumerate() { @@ -42,34 +42,36 @@ fn extract_words(line: &str) -> Option<(usize, Vec)> { } } } + if words == &[String::new()] { + return None; + } reverse_string(words.last_mut().unwrap()); words.reverse(); + Some((startpos, words)) } -impl<'a> ShellHelper<'a> { - pub fn new(vm: &'a VirtualMachine, scope: Scope) -> Self { +impl<'vm> ShellHelper<'vm> { + pub fn new(vm: &'vm VirtualMachine, scope: Scope) -> Self { ShellHelper { vm, scope } } - // fn get_words - - fn complete_opt(&self, line: &str) -> Option<(usize, Vec)> { - let (startpos, words) = extract_words(line)?; - + fn get_available_completions<'w>( + &self, + words: &'w [String], + ) -> Option<( + &'w str, + Box> + 'vm>, + )> { // the very first word and then all the ones after the dot let (first, rest) = words.split_first().unwrap(); - let str_iter = |obj| { - PyIterable::::try_from_object(self.vm, obj) - .ok()? - .iter(self.vm) - .ok() + let str_iter_method = |obj, name| { + let iter = self.vm.call_method(obj, name, vec![])?; + PyIterable::::try_from_object(self.vm, iter)?.iter(self.vm) }; - type StrIter<'a> = Box> + 'a>; - - let (iter, prefix) = if let Some((last, parents)) = rest.split_last() { + if let Some((last, parents)) = rest.split_last() { // we need to get an attribute based off of the dir() of an object // last: the last word, could be empty if it ends with a dot @@ -81,58 +83,55 @@ impl<'a> ShellHelper<'a> { current = self.vm.get_attribute(current.clone(), attr.as_str()).ok()?; } - ( - Box::new(str_iter( - self.vm.call_method(¤t, "__dir__", vec![]).ok()?, - )?) as StrIter, - last.as_str(), - ) + let current_iter = str_iter_method(¤t, "__dir__").ok()?; + + Some((&last, Box::new(current_iter) as _)) } else { // we need to get a variable based off of globals/builtins - let globals = str_iter( - self.vm - .call_method(self.scope.globals.as_object(), "keys", vec![]) - .ok()?, - )?; - let iter = if first.as_str().is_empty() { - // only show globals that don't start with a '_' - Box::new(globals.filter(|r| { - r.as_ref() - .ok() - .map_or(true, |s| !s.as_str().starts_with('_')) - })) as StrIter - } else { - // show globals and builtins - Box::new( - globals.chain(str_iter( - self.vm - .call_method(&self.vm.builtins, "__dir__", vec![]) - .ok()?, - )?), - ) as StrIter - }; - (iter, first.as_str()) - }; - let completions = iter + let globals = str_iter_method(self.scope.globals.as_object(), "keys").ok()?; + let builtins = str_iter_method(&self.vm.builtins, "__dir__").ok()?; + Some((&first, Box::new(Iterator::chain(globals, builtins)) as _)) + } + } + + fn complete_opt(&self, line: &str) -> Option<(usize, Vec)> { + let (startpos, words) = split_idents_on_dot(line)?; + + let (word_start, iter) = self.get_available_completions(&words)?; + + let all_completions = iter .filter(|res| { res.as_ref() .ok() - .map_or(true, |s| s.as_str().starts_with(prefix)) + .map_or(true, |s| s.as_str().starts_with(word_start)) }) .collect::, _>>() .ok()?; - let no_underscore = completions - .iter() - .cloned() - .filter(|s| !prefix.starts_with('_') && !s.as_str().starts_with('_')) - .collect::>(); - let mut completions = if no_underscore.is_empty() { - completions + let mut completions = if word_start.starts_with('_') { + // if they're already looking for something starting with a '_', just give + // them all the completions + all_completions } else { - no_underscore + // only the completions that don't start with a '_' + let no_underscore = all_completions + .iter() + .cloned() + .filter(|s| !s.as_str().starts_with('_')) + .collect::>(); + + // if there are only completions that start with a '_', give them all of the + // completions, otherwise only the ones that don't start with '_' + if no_underscore.is_empty() { + all_completions + } else { + no_underscore + } }; + + // sort the completions alphabetically completions.sort_by(|a, b| std::cmp::Ord::cmp(a.as_str(), b.as_str())); + Some(( startpos, completions