Skip to content

Commit fc3e8c2

Browse files
committed
Add tab autocompletion to the REPL
1 parent dccc22d commit fc3e8c2

File tree

3 files changed

+304
-21
lines changed

3 files changed

+304
-21
lines changed

Cargo.lock

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.rs

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ use rustpython_compiler::{compile, error::CompileError, error::CompileErrorType}
99
use rustpython_parser::error::ParseErrorType;
1010
use rustpython_vm::{
1111
import, match_class,
12-
obj::{objint::PyInt, objtuple::PyTuple, objtype},
12+
obj::{objint::PyInt, objstr::PyStringRef, objtuple::PyTuple, objtype},
1313
print_exception,
14-
pyobject::{ItemProtocol, PyObjectRef, PyResult},
15-
scope::Scope,
14+
pyobject::{ItemProtocol, PyIterable, PyObjectRef, PyResult, TryFromObject},
15+
scope::{NameProtocol, Scope},
1616
util, PySettings, VirtualMachine,
1717
};
1818
use std::convert::TryInto;
@@ -487,18 +487,144 @@ fn shell_exec(vm: &VirtualMachine, source: &str, scope: Scope) -> ShellExecResul
487487
}
488488
}
489489

490+
struct ShellHelper<'a> {
491+
vm: &'a VirtualMachine,
492+
scope: Scope,
493+
}
494+
495+
impl ShellHelper<'_> {
496+
fn complete_opt(&self, line: &str) -> Option<(usize, Vec<String>)> {
497+
let mut words = vec![String::new()];
498+
fn revlastword(words: &mut Vec<String>) {
499+
let word = words.last_mut().unwrap();
500+
let revword = word.chars().rev().collect();
501+
*word = revword;
502+
}
503+
let mut startpos = 0;
504+
for (i, c) in line.chars().rev().enumerate() {
505+
match c {
506+
'.' => {
507+
// check for a double dot
508+
if i != 0 && words.last().map_or(false, |s| s.is_empty()) {
509+
return None;
510+
}
511+
revlastword(&mut words);
512+
if words.len() == 1 {
513+
startpos = line.len() - i;
514+
}
515+
words.push(String::new());
516+
}
517+
c if c.is_alphanumeric() || c == '_' => words.last_mut().unwrap().push(c),
518+
_ => {
519+
if words.len() == 1 {
520+
if words.last().unwrap().is_empty() {
521+
return None;
522+
}
523+
startpos = line.len() - i;
524+
}
525+
break;
526+
}
527+
}
528+
}
529+
revlastword(&mut words);
530+
words.reverse();
531+
532+
// the very first word and then all the ones after the dot
533+
let (first, rest) = words.split_first().unwrap();
534+
535+
let (iter, prefix) = if let Some((last, parents)) = rest.split_last() {
536+
// last: the last word, could be empty if it ends with a dot
537+
// parents: the words before the dot
538+
539+
let mut current = self.scope.load_global(self.vm, first)?;
540+
541+
for attr in parents {
542+
current = self.vm.get_attribute(current.clone(), attr.as_str()).ok()?;
543+
}
544+
545+
(
546+
self.vm.call_method(&current, "__dir__", vec![]).ok()?,
547+
last.as_str(),
548+
)
549+
} else {
550+
(
551+
self.vm
552+
.call_method(self.scope.globals.as_object(), "keys", vec![])
553+
.ok()?,
554+
first.as_str(),
555+
)
556+
};
557+
let iter = PyIterable::<PyStringRef>::try_from_object(self.vm, iter).ok()?;
558+
let completions = iter
559+
.iter(self.vm)
560+
.ok()?
561+
.filter(|res| {
562+
res.as_ref()
563+
.ok()
564+
.map_or(true, |s| s.as_str().starts_with(prefix))
565+
})
566+
.collect::<Result<Vec<_>, _>>()
567+
.ok()?;
568+
let no_underscore = completions
569+
.iter()
570+
.cloned()
571+
.filter(|s| !prefix.starts_with("_") && !s.as_str().starts_with("_"))
572+
.collect::<Vec<_>>();
573+
let completions = if no_underscore.is_empty() {
574+
completions
575+
} else {
576+
no_underscore
577+
};
578+
Some((
579+
startpos,
580+
completions
581+
.into_iter()
582+
.map(|s| s.as_str().to_owned())
583+
.collect(),
584+
))
585+
}
586+
}
587+
588+
impl rustyline::completion::Completer for ShellHelper<'_> {
589+
type Candidate = String;
590+
591+
fn complete(
592+
&self,
593+
line: &str,
594+
pos: usize,
595+
_ctx: &rustyline::Context,
596+
) -> rustyline::Result<(usize, Vec<String>)> {
597+
if pos != line.len() {
598+
return Ok((0, vec![]));
599+
}
600+
Ok(self.complete_opt(line).unwrap_or((0, vec![])))
601+
}
602+
}
603+
604+
impl rustyline::hint::Hinter for ShellHelper<'_> {}
605+
impl rustyline::highlight::Highlighter for ShellHelper<'_> {}
606+
impl rustyline::Helper for ShellHelper<'_> {}
607+
490608
#[cfg(not(target_os = "wasi"))]
491609
fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
492-
use rustyline::{error::ReadlineError, Editor};
610+
use rustyline::{error::ReadlineError, CompletionType, Config, Editor};
493611

494612
println!(
495613
"Welcome to the magnificent Rust Python {} interpreter \u{1f631} \u{1f596}",
496614
crate_version!()
497615
);
498616

499617
// Read a single line:
500-
let mut input = String::new();
501-
let mut repl = Editor::<()>::new();
618+
let mut repl = Editor::with_config(
619+
Config::builder()
620+
.completion_type(CompletionType::List)
621+
.build(),
622+
);
623+
repl.set_helper(Some(ShellHelper {
624+
vm,
625+
scope: scope.clone(),
626+
}));
627+
let mut full_input = String::new();
502628

503629
// Retrieve a `history_path_str` dependent on the OS
504630
let repl_history_path = match dirs::config_dir() {
@@ -539,12 +665,12 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
539665

540666
let stop_continuing = line.is_empty();
541667

542-
if input.is_empty() {
543-
input = line;
668+
if full_input.is_empty() {
669+
full_input = line;
544670
} else {
545-
input.push_str(&line);
671+
full_input.push_str(&line);
546672
}
547-
input.push_str("\n");
673+
full_input.push_str("\n");
548674

549675
if continuing {
550676
if stop_continuing {
@@ -554,24 +680,24 @@ fn run_shell(vm: &VirtualMachine, scope: Scope) -> PyResult<()> {
554680
}
555681
}
556682

557-
match shell_exec(vm, &input, scope.clone()) {
683+
match shell_exec(vm, &full_input, scope.clone()) {
558684
ShellExecResult::Ok => {
559-
input.clear();
685+
full_input.clear();
560686
Ok(())
561687
}
562688
ShellExecResult::Continue => {
563689
continuing = true;
564690
Ok(())
565691
}
566692
ShellExecResult::PyErr(err) => {
567-
input.clear();
693+
full_input.clear();
568694
Err(err)
569695
}
570696
}
571697
}
572698
Err(ReadlineError::Interrupted) => {
573699
continuing = false;
574-
input.clear();
700+
full_input.clear();
575701
let keyboard_interrupt = vm
576702
.new_empty_exception(vm.ctx.exceptions.keyboard_interrupt.clone())
577703
.unwrap();

0 commit comments

Comments
 (0)