From fc3e8c2483541476a3e583bef91dac328af72b10 Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Fri, 4 Oct 2019 19:59:04 +0000 Subject: [PATCH 1/6] Add tab autocompletion to the REPL --- Cargo.lock | 7 -- src/main.rs | 154 +++++++++++++++++++++++++++++++++++++---- src/shell_helper.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+), 21 deletions(-) create mode 100644 src/shell_helper.rs diff --git a/Cargo.lock b/Cargo.lock index d78a65543..2ac962772 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,11 +374,6 @@ 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" @@ -1171,7 +1166,6 @@ 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)", @@ -2061,7 +2055,6 @@ 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 78f90f365..406cfd097 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, objtuple::PyTuple, objtype}, + obj::{objint::PyInt, objstr::PyStringRef, objtuple::PyTuple, objtype}, print_exception, - pyobject::{ItemProtocol, PyObjectRef, PyResult}, - scope::Scope, + pyobject::{ItemProtocol, PyIterable, PyObjectRef, PyResult, TryFromObject}, + scope::{NameProtocol, Scope}, util, PySettings, VirtualMachine, }; use std::convert::TryInto; @@ -487,9 +487,127 @@ 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 (iter, prefix) = if let Some((last, parents)) = rest.split_last() { + // 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()?; + } + + ( + self.vm.call_method(¤t, "__dir__", vec![]).ok()?, + last.as_str(), + ) + } else { + ( + self.vm + .call_method(self.scope.globals.as_object(), "keys", vec![]) + .ok()?, + first.as_str(), + ) + }; + let iter = PyIterable::::try_from_object(self.vm, iter).ok()?; + let completions = iter + .iter(self.vm) + .ok()? + .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 completions = if no_underscore.is_empty() { + completions + } else { + no_underscore + }; + 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, Editor}; + use rustyline::{error::ReadlineError, CompletionType, Config, Editor}; println!( "Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}", @@ -497,8 +615,16 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { ); // Read a single line: - let mut input = String::new(); - let mut repl = Editor::<()>::new(); + let mut repl = Editor::with_config( + Config::builder() + .completion_type(CompletionType::List) + .build(), + ); + repl.set_helper(Some(ShellHelper { + vm, + scope: scope.clone(), + })); + let mut full_input = String::new(); // Retrieve a `history_path_str` dependent on the OS let repl_history_path = match dirs::config_dir() { @@ -539,12 +665,12 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { let stop_continuing = line.is_empty(); - if input.is_empty() { - input = line; + if full_input.is_empty() { + full_input = line; } else { - input.push_str(&line); + full_input.push_str(&line); } - input.push_str("\n"); + full_input.push_str("\n"); if continuing { if stop_continuing { @@ -554,9 +680,9 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { } } - match shell_exec(vm, &input, scope.clone()) { + match shell_exec(vm, &full_input, scope.clone()) { ShellExecResult::Ok => { - input.clear(); + full_input.clear(); Ok(()) } ShellExecResult::Continue => { @@ -564,14 +690,14 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { Ok(()) } ShellExecResult::PyErr(err) => { - input.clear(); + full_input.clear(); Err(err) } } } Err(ReadlineError::Interrupted) => { continuing = false; - input.clear(); + full_input.clear(); let keyboard_interrupt = vm .new_empty_exception(vm.ctx.exceptions.keyboard_interrupt.clone()) .unwrap(); diff --git a/src/shell_helper.rs b/src/shell_helper.rs new file mode 100644 index 000000000..a20f76275 --- /dev/null +++ b/src/shell_helper.rs @@ -0,0 +1,164 @@ +use rustpython_vm::obj::objstr::PyStringRef; +use rustpython_vm::pyobject::{PyIterable, PyResult, TryFromObject}; +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, + scope: Scope, +} + +fn reverse_string(s: &mut String) { + let rev = s.chars().rev().collect(); + *s = rev; +} + +fn extract_words(line: &str) -> Option<(usize, Vec)> { + let mut words = vec![String::new()]; + 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; + } + reverse_string(words.last_mut().unwrap()); + 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; + } + } + } + reverse_string(words.last_mut().unwrap()); + words.reverse(); + Some((startpos, words)) +} + +impl<'a> ShellHelper<'a> { + pub fn new(vm: &'a 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)?; + + // 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 Completer for ShellHelper<'_> { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context, + ) -> rustyline::Result<(usize, Vec)> { + if pos != line.len() { + return Ok((0, vec![])); + } + Ok(self.complete_opt(line).unwrap_or((0, vec![]))) + } +} + +impl Hinter for ShellHelper<'_> {} +impl Highlighter for ShellHelper<'_> {} +impl Helper for ShellHelper<'_> {} From de7002a3e723e0bb65e788e778815afdb3b70e97 Mon Sep 17 00:00:00 2001 From: Noah <33094578+coolreader18@users.noreply.github.com> Date: Fri, 4 Oct 2019 20:45:33 +0000 Subject: [PATCH 2/6] Don't show builtins when tabbing on an empty line --- src/main.rs | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 406cfd097..e7d97bdcd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -532,7 +532,18 @@ impl ShellHelper<'_> { // 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 @@ -543,21 +554,39 @@ impl ShellHelper<'_> { } ( - self.vm.call_method(¤t, "__dir__", vec![]).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()?, - first.as_str(), - ) + )?; + 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 iter = PyIterable::::try_from_object(self.vm, iter).ok()?; let completions = iter - .iter(self.vm) - .ok()? .filter(|res| { res.as_ref() .ok() @@ -568,13 +597,14 @@ impl ShellHelper<'_> { let no_underscore = completions .iter() .cloned() - .filter(|s| !prefix.starts_with("_") && !s.as_str().starts_with("_")) + .filter(|s| !prefix.starts_with('_') && !s.as_str().starts_with('_')) .collect::>(); - let completions = if no_underscore.is_empty() { + 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 From 04a5aecc3d20178513abcbb210fd4349f60d4f48 Mon Sep 17 00:00:00 2001 From: coolreader18 <33094578+coolreader18@users.noreply.github.com> Date: Tue, 8 Oct 2019 18:28:26 -0500 Subject: [PATCH 3/6] Move ShellHelper into another module --- Cargo.lock | 7 ++ src/main.rs | 161 ++------------------------------------------ src/shell_helper.rs | 115 ++++++++++++++++--------------- 3 files changed, 70 insertions(+), 213 deletions(-) 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 From 2220ed7e811bfc68b4fed862b127351503832abd Mon Sep 17 00:00:00 2001 From: coolreader18 <33094578+coolreader18@users.noreply.github.com> Date: Tue, 8 Oct 2019 20:00:28 -0500 Subject: [PATCH 4/6] Abstract over the readline implementation --- src/main.rs | 233 +--------------- src/shell.rs | 259 ++++++++++++++++++ .../rustyline_helper.rs} | 3 +- 3 files changed, 266 insertions(+), 229 deletions(-) create mode 100644 src/shell.rs rename src/{shell_helper.rs => shell/rustyline_helper.rs} (98%) diff --git a/src/main.rs b/src/main.rs index 6291d61fd..6745f1ae0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,24 +5,23 @@ extern crate env_logger; extern crate log; use clap::{App, AppSettings, Arg, ArgMatches}; -use rustpython_compiler::{compile, error::CompileError, error::CompileErrorType}; -use rustpython_parser::error::ParseErrorType; +use rustpython_compiler::compile; use rustpython_vm::{ import, match_class, obj::{objint::PyInt, objtuple::PyTuple, objtype}, print_exception, - pyobject::{ItemProtocol, PyObjectRef, PyResult}, + pyobject::{ItemProtocol, PyResult}, scope::Scope, util, PySettings, VirtualMachine, }; -use std::convert::TryInto; +use std::convert::TryInto; use std::env; use std::path::PathBuf; use std::process; use std::str::FromStr; -mod shell_helper; +mod shell; fn main() { #[cfg(feature = "flame-it")] @@ -367,7 +366,7 @@ fn run_rustpython(vm: &VirtualMachine, matches: &ArgMatches) -> PyResult<()> { } else if let Some(filename) = matches.value_of("script") { run_script(&vm, scope, filename)? } else { - run_shell(&vm, scope)?; + shell::run_shell(&vm, scope)?; } Ok(()) @@ -459,225 +458,3 @@ fn test_run_script() { let r = run_script(&vm, vm.new_scope_with_builtins(), "tests/snippets/dir_main"); assert!(r.is_ok()); } - -enum ShellExecResult { - Ok, - PyErr(PyObjectRef), - Continue, -} - -fn shell_exec(vm: &VirtualMachine, source: &str, scope: Scope) -> ShellExecResult { - match vm.compile(source, compile::Mode::Single, "".to_string()) { - Ok(code) => { - match vm.run_code_obj(code, scope.clone()) { - Ok(value) => { - // Save non-None values as "_" - if !vm.is_none(&value) { - let key = "_"; - scope.globals.set_item(key, value, vm).unwrap(); - } - ShellExecResult::Ok - } - Err(err) => ShellExecResult::PyErr(err), - } - } - Err(CompileError { - error: CompileErrorType::Parse(ParseErrorType::EOF), - .. - }) => ShellExecResult::Continue, - Err(err) => ShellExecResult::PyErr(vm.new_syntax_error(&err)), - } -} - -#[cfg(not(target_os = "wasi"))] -fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { - use rustyline::{error::ReadlineError, CompletionType, Config, Editor}; - - println!( - "Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}", - crate_version!() - ); - - // Read a single line: - let mut repl = Editor::with_config( - Config::builder() - .completion_type(CompletionType::List) - .build(), - ); - 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 - let repl_history_path = match dirs::config_dir() { - Some(mut path) => { - path.push("rustpython"); - path.push("repl_history.txt"); - path - } - None => ".repl_history.txt".into(), - }; - - if !repl_history_path.exists() { - if let Some(parent) = repl_history_path.parent() { - std::fs::create_dir_all(parent).unwrap(); - } - } - - if repl.load_history(&repl_history_path).is_err() { - println!("No previous history."); - } - - let mut continuing = false; - - loop { - let prompt_name = if continuing { "ps2" } else { "ps1" }; - let prompt = vm - .get_attribute(vm.sys_module.clone(), prompt_name) - .and_then(|prompt| vm.to_str(&prompt)); - let prompt = match prompt { - Ok(ref s) => s.as_str(), - Err(_) => "", - }; - let result = match repl.readline(prompt) { - Ok(line) => { - debug!("You entered {:?}", line); - - repl.add_history_entry(line.trim_end()); - - let stop_continuing = line.is_empty(); - - if full_input.is_empty() { - full_input = line; - } else { - full_input.push_str(&line); - } - full_input.push_str("\n"); - - if continuing { - if stop_continuing { - continuing = false; - } else { - continue; - } - } - - match shell_exec(vm, &full_input, scope.clone()) { - ShellExecResult::Ok => { - full_input.clear(); - Ok(()) - } - ShellExecResult::Continue => { - continuing = true; - Ok(()) - } - ShellExecResult::PyErr(err) => { - full_input.clear(); - Err(err) - } - } - } - Err(ReadlineError::Interrupted) => { - continuing = false; - full_input.clear(); - let keyboard_interrupt = vm - .new_empty_exception(vm.ctx.exceptions.keyboard_interrupt.clone()) - .unwrap(); - Err(keyboard_interrupt) - } - Err(ReadlineError::Eof) => { - break; - } - Err(err) => { - eprintln!("Readline error: {:?}", err); - break; - } - }; - - if let Err(exc) = result { - if objtype::isinstance(&exc, &vm.ctx.exceptions.system_exit) { - repl.save_history(&repl_history_path).unwrap(); - return Err(exc); - } - print_exception(vm, &exc); - } - } - repl.save_history(&repl_history_path).unwrap(); - - Ok(()) -} - -#[cfg(target_os = "wasi")] -fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { - use std::io::prelude::*; - use std::io::{self, BufRead}; - - println!( - "Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}", - crate_version!() - ); - - // Read a single line: - let mut input = String::new(); - let mut continuing = false; - - loop { - let prompt_name = if continuing { "ps2" } else { "ps1" }; - let prompt = vm - .get_attribute(vm.sys_module.clone(), prompt_name) - .and_then(|prompt| vm.to_str(&prompt)); - let prompt = match prompt { - Ok(ref s) => s.as_str(), - Err(_) => "", - }; - print!("{}", prompt); - io::stdout().flush().ok().expect("Could not flush stdout"); - - let stdin = io::stdin(); - - let result = match stdin.lock().lines().next().unwrap() { - Ok(line) => { - debug!("You entered {:?}", line); - let stop_continuing = line.is_empty(); - - if input.is_empty() { - input = line; - } else { - input.push_str(&line); - } - input.push_str("\n"); - - if continuing { - if stop_continuing { - continuing = false; - } else { - continue; - } - } - - match shell_exec(vm, &input, scope.clone()) { - ShellExecResult::Ok => { - input.clear(); - Ok(()) - } - ShellExecResult::Continue => { - continuing = true; - Ok(()) - } - ShellExecResult::PyErr(err) => { - input.clear(); - Err(err) - } - } - } - Err(err) => { - eprintln!("Readline error: {:?}", err); - break; - } - }; - - if let Err(exc) = result { - print_exception(vm, &exc); - } - } - Ok(()) -} diff --git a/src/shell.rs b/src/shell.rs new file mode 100644 index 000000000..5fe26186c --- /dev/null +++ b/src/shell.rs @@ -0,0 +1,259 @@ +#[cfg(not(target_os = "wasi"))] +mod rustyline_helper; + +use rustpython_compiler::{compile, error::CompileError, error::CompileErrorType}; +use rustpython_parser::error::ParseErrorType; +use rustpython_vm::{ + obj::objtype, + print_exception, + pyobject::{ItemProtocol, PyObjectRef, PyResult}, + scope::Scope, + VirtualMachine, +}; +#[cfg(not(target_os = "wasi"))] +use rustyline_helper::ShellHelper; + +use std::io; +use std::path::Path; + +enum ShellExecResult { + Ok, + PyErr(PyObjectRef), + Continue, +} + +fn shell_exec(vm: &VirtualMachine, source: &str, scope: Scope) -> ShellExecResult { + match vm.compile(source, compile::Mode::Single, "".to_string()) { + Ok(code) => { + match vm.run_code_obj(code, scope.clone()) { + Ok(value) => { + // Save non-None values as "_" + if !vm.is_none(&value) { + let key = "_"; + scope.globals.set_item(key, value, vm).unwrap(); + } + ShellExecResult::Ok + } + Err(err) => ShellExecResult::PyErr(err), + } + } + Err(CompileError { + error: CompileErrorType::Parse(ParseErrorType::EOF), + .. + }) => ShellExecResult::Continue, + Err(err) => ShellExecResult::PyErr(vm.new_syntax_error(&err)), + } +} + +enum ReadlineResult { + Line(String), + EOF, + Interrupt, + IO(std::io::Error), + EncodingError, + Other(Box), +} + +#[allow(unused)] +struct BasicReadline; + +#[allow(unused)] +impl BasicReadline { + fn new(_vm: &VirtualMachine, _scope: Scope) -> Self { + BasicReadline + } + + fn load_history(&mut self, _path: &Path) -> io::Result<()> { + Ok(()) + } + + fn save_history(&mut self, _path: &Path) -> io::Result<()> { + Ok(()) + } + + fn add_history_entry(&mut self, _entry: &str) {} + + fn readline(&mut self, prompt: &str) -> ReadlineResult { + use std::io::prelude::*; + print!("{}", prompt); + if let Err(e) = io::stdout().flush() { + return ReadlineResult::IO(e); + } + + match io::stdin().lock().lines().next() { + Some(Ok(line)) => ReadlineResult::Line(line), + None => ReadlineResult::EOF, + Some(Err(e)) => match e.kind() { + io::ErrorKind::Interrupted => ReadlineResult::Interrupt, + io::ErrorKind::InvalidData => ReadlineResult::EncodingError, + _ => ReadlineResult::IO(e), + }, + } + } +} + +#[cfg(target_os = "wasi")] +type Readline = BasicReadline; + +#[cfg(not(target_os = "wasi"))] +struct RustylineReadline<'vm> { + repl: rustyline::Editor>, +} +#[cfg(not(target_os = "wasi"))] +impl<'vm> RustylineReadline<'vm> { + fn new(vm: &'vm VirtualMachine, scope: Scope) -> Self { + use rustyline::{CompletionType, Config, Editor}; + let mut repl = Editor::with_config( + Config::builder() + .completion_type(CompletionType::List) + .build(), + ); + repl.set_helper(Some(ShellHelper::new(vm, scope))); + RustylineReadline { repl } + } + + fn load_history(&mut self, path: &Path) -> rustyline::Result<()> { + self.repl.load_history(path) + } + + fn save_history(&mut self, path: &Path) -> rustyline::Result<()> { + if !path.exists() { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + } + self.repl.save_history(path) + } + + fn add_history_entry(&mut self, entry: &str) { + self.repl.add_history_entry(entry); + } + + fn readline(&mut self, prompt: &str) -> ReadlineResult { + use rustyline::error::ReadlineError; + match self.repl.readline(prompt) { + Ok(line) => ReadlineResult::Line(line), + Err(ReadlineError::Interrupted) => ReadlineResult::Interrupt, + Err(ReadlineError::Eof) => ReadlineResult::EOF, + Err(ReadlineError::Io(e)) => ReadlineResult::IO(e), + #[cfg(unix)] + Err(ReadlineError::Utf8Error) => ReadlineResult::EncodingError, + #[cfg(windows)] + Err(ReadlineError::Decode(_)) => ReadlineResult::EncodingError, + Err(e) => ReadlineResult::Other(e.into()), + } + } +} + +#[cfg(not(target_os = "wasi"))] +type Readline<'a> = RustylineReadline<'a>; + +pub fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> { + println!( + "Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}", + crate_version!() + ); + + let mut repl = Readline::new(vm, scope.clone()); + let mut full_input = String::new(); + + // Retrieve a `history_path_str` dependent on the OS + let repl_history_path = match dirs::config_dir() { + Some(mut path) => { + path.push("rustpython"); + path.push("repl_history.txt"); + path + } + None => ".repl_history.txt".into(), + }; + + if repl.load_history(&repl_history_path).is_err() { + println!("No previous history."); + } + + let mut continuing = false; + + loop { + let prompt_name = if continuing { "ps2" } else { "ps1" }; + let prompt = vm + .get_attribute(vm.sys_module.clone(), prompt_name) + .and_then(|prompt| vm.to_str(&prompt)); + let prompt = match prompt { + Ok(ref s) => s.as_str(), + Err(_) => "", + }; + let result = match repl.readline(prompt) { + ReadlineResult::Line(line) => { + debug!("You entered {:?}", line); + + repl.add_history_entry(line.trim_end()); + + let stop_continuing = line.is_empty(); + + if full_input.is_empty() { + full_input = line; + } else { + full_input.push_str(&line); + } + full_input.push_str("\n"); + + if continuing { + if stop_continuing { + continuing = false; + } else { + continue; + } + } + + match shell_exec(vm, &full_input, scope.clone()) { + ShellExecResult::Ok => { + full_input.clear(); + Ok(()) + } + ShellExecResult::Continue => { + continuing = true; + Ok(()) + } + ShellExecResult::PyErr(err) => { + full_input.clear(); + Err(err) + } + } + } + ReadlineResult::Interrupt => { + continuing = false; + full_input.clear(); + let keyboard_interrupt = vm + .new_empty_exception(vm.ctx.exceptions.keyboard_interrupt.clone()) + .unwrap(); + Err(keyboard_interrupt) + } + ReadlineResult::EOF => { + break; + } + ReadlineResult::EncodingError => { + eprintln!("Invalid UTF-8 entered"); + Ok(()) + } + ReadlineResult::Other(err) => { + eprintln!("Readline error: {:?}", err); + break; + } + ReadlineResult::IO(err) => { + eprintln!("IO error: {:?}", err); + break; + } + }; + + if let Err(exc) = result { + if objtype::isinstance(&exc, &vm.ctx.exceptions.system_exit) { + repl.save_history(&repl_history_path).unwrap(); + return Err(exc); + } + print_exception(vm, &exc); + } + } + repl.save_history(&repl_history_path).unwrap(); + + Ok(()) +} diff --git a/src/shell_helper.rs b/src/shell/rustyline_helper.rs similarity index 98% rename from src/shell_helper.rs rename to src/shell/rustyline_helper.rs index ac1e2d5cd..2cbe0a0db 100644 --- a/src/shell_helper.rs +++ b/src/shell/rustyline_helper.rs @@ -42,7 +42,7 @@ fn split_idents_on_dot(line: &str) -> Option<(usize, Vec)> { } } } - if words == &[String::new()] { + if words == [String::new()] { return None; } reverse_string(words.last_mut().unwrap()); @@ -56,6 +56,7 @@ impl<'vm> ShellHelper<'vm> { ShellHelper { vm, scope } } + #[allow(clippy::type_complexity)] fn get_available_completions<'w>( &self, words: &'w [String], From e7a6b79023be12b7112051a86e82cfc5d123bc7e Mon Sep 17 00:00:00 2001 From: coolreader18 <33094578+coolreader18@users.noreply.github.com> Date: Tue, 8 Oct 2019 21:26:34 -0500 Subject: [PATCH 5/6] Allow indenting by pressing tab --- src/shell.rs | 1 + src/shell/rustyline_helper.rs | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/shell.rs b/src/shell.rs index 5fe26186c..d0ccd7098 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -106,6 +106,7 @@ impl<'vm> RustylineReadline<'vm> { let mut repl = Editor::with_config( Config::builder() .completion_type(CompletionType::List) + .tab_stop(4) .build(), ); repl.set_helper(Some(ShellHelper::new(vm, scope))); diff --git a/src/shell/rustyline_helper.rs b/src/shell/rustyline_helper.rs index 2cbe0a0db..d8bede5df 100644 --- a/src/shell/rustyline_helper.rs +++ b/src/shell/rustyline_helper.rs @@ -155,7 +155,11 @@ impl Completer for ShellHelper<'_> { if pos != line.len() { return Ok((0, vec![])); } - Ok(self.complete_opt(line).unwrap_or((0, vec![]))) + Ok(self + .complete_opt(line) + // as far as I can tell, there's no better way to do both completion + // and indentation (or even just indentation) + .unwrap_or_else(|| (line.len(), vec![" ".to_string()]))) } } From 1a68c86f92ceb27ae46b056d4605a6c463367c88 Mon Sep 17 00:00:00 2001 From: coolreader18 <33094578+coolreader18@users.noreply.github.com> Date: Tue, 8 Oct 2019 21:52:39 -0500 Subject: [PATCH 6/6] Update wapm.toml --- wapm.toml | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/wapm.toml b/wapm.toml index 6c0575ee8..4fc70367f 100644 --- a/wapm.toml +++ b/wapm.toml @@ -1,20 +1,17 @@ [package] -name="rustpython" -version="0.0.4" -description="A Python-3 (CPython >= 3.5.0) Interpreter written in Rust 🐍 😱 🤘" -license-file="LICENSE" +name = "rustpython" +version = "0.1.1" +description = "A Python-3 (CPython >= 3.5.0) Interpreter written in Rust 🐍 😱 🤘" +license-file = "LICENSE" readme = "README.md" repository = "https://github.com/RustPython/RustPython" [[module]] -name="rustpython" -source="target/wasm32-wasi/release/rustpython.wasm" +name = "rustpython" +source = "target/wasm32-wasi/release/rustpython.wasm" abi = "wasi" -interfaces = {wasi= "0.0.0-unstable"} +interfaces = { wasi = "0.0.0-unstable" } [[command]] -name="rustpython" -module="rustpython" - -# [fs] -# "Lib"="Lib" +name = "rustpython" +module = "rustpython"